brocoders/nestjs-boilerplate

0 errors, 21 warningsmain

No critical or high-risk findings. 7 worth a look.

21 warnings in the paths between 19 routes and 5 tables.

ROUTE FINDINGS9 of 19 · 12/12 rules
ROUTES
19
TABLES
5
FILES READ
176/176
RULES RUN
12/12

21 findings

7 medium14 low

  • Mediumlow confidencePOST /login has no validator or rate limiter attached.

    POST /login accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/login' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /login rejects an unexpected body", async () => {
      const res = await request(app).post("/login").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /email/login has no validator or rate limiter attached.

    POST /email/login accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/email/login' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /email/login rejects an unexpected body", async () => {
      const res = await request(app).post("/email/login").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /email/register has no validator or rate limiter attached.

    POST /email/register accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/email/register' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /email/register rejects an unexpected body", async () => {
      const res = await request(app).post("/email/register").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /email/confirm has no validator or rate limiter attached.

    POST /email/confirm accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/email/confirm' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /email/confirm rejects an unexpected body", async () => {
      const res = await request(app).post("/email/confirm").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /email/confirm/new has no validator or rate limiter attached.

    POST /email/confirm/new accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/email/confirm/new' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /email/confirm/new rejects an unexpected body", async () => {
      const res = await request(app).post("/email/confirm/new").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /forgot/password has no validator or rate limiter attached.

    POST /forgot/password accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/forgot/password' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /forgot/password rejects an unexpected body", async () => {
      const res = await request(app).post("/forgot/password").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Mediumlow confidencePOST /reset/password has no validator or rate limiter attached.

    POST /reset/password accepts any body at any rate. Malformed input reaches your handler, and nothing stops a script calling it in a loop.

    Missing
    Schema validation of the request body, and a rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Prove it

    curl -i -X POST 'https://YOUR_API/reset/password' \
      -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}'
    # Expect 400. A 2xx or 500 means the body is not validated.

    Fix

    const Body = z.object({ name: z.string().max(200) }).strict();
    
    const parsed = Body.safeParse(req.body);
    if (!parsed.success) return res.status(400).json(parsed.error.flatten());

    Regression test

    test("POST /reset/password rejects an unexpected body", async () => {
      const res = await request(app).post("/reset/password").send({ unexpected: true });
      expect(res.status).toBe(400);
    });

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Lowhigh confidencePOST /login reaches session without auth, which is expected for an authentication endpoint.

    POST /login must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    session (write) · sensitive: hash
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /login → session

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/login", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /login is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/login").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/login").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /login reaches user without auth, which is expected for an authentication endpoint.

    POST /login must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (write) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /login → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/login", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /login is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/login").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/login").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /email/login reaches session without auth, which is expected for an authentication endpoint.

    POST /email/login must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    session (write) · sensitive: hash
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /email/login → session

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/email/login", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /email/login is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/email/login").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/email/login").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /email/login reaches user without auth, which is expected for an authentication endpoint.

    POST /email/login must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (read) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /email/login → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/email/login", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /email/login is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/email/login").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/email/login").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /email/register reaches user without auth, which is expected for an authentication endpoint.

    POST /email/register must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (write) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /email/register → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/email/register", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /email/register is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/email/register").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/email/register").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /email/confirm reaches user without auth, which is expected for an authentication endpoint.

    POST /email/confirm must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (write) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /email/confirm → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/email/confirm", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /email/confirm is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/email/confirm").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/email/confirm").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /email/confirm/new reaches user without auth, which is expected for an authentication endpoint.

    POST /email/confirm/new must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (write) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /email/confirm/new → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/email/confirm/new", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /email/confirm/new is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/email/confirm/new").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/email/confirm/new").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /forgot/password reaches user without auth, which is expected for an authentication endpoint.

    POST /forgot/password must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (read) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /forgot/password → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/forgot/password", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /forgot/password is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/forgot/password").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/forgot/password").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /reset/password reaches session without auth, which is expected for an authentication endpoint.

    POST /reset/password must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    session (write) · sensitive: hash
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /reset/password → session

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/reset/password", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /reset/password is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/reset/password").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/reset/password").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencePOST /reset/password reaches user without auth, which is expected for an authentication endpoint.

    POST /reset/password must be public — it is how people sign in. It is also where credential stuffing and account enumeration arrive.

    Data reached
    user (write) · sensitive: email, password
    Missing
    A tight rate limit and identical responses for unknown users and wrong passwords.
    Confidence
    Identified as an authentication endpoint by its path, touching identity tables only.
    Path
    POST /reset/password → user

    Fix

    // 5 attempts per IP per minute on sign-in.
    app.post("/reset/password", rateLimit({ windowMs: 60_000, limit: 5 }), handler);

    Regression test

    test("POST /reset/password is rate limited", async () => {
      for (let i = 0; i < 5; i++) await request(app).post("/reset/password").send({ email: "a@b.c", password: "x" });
      const res = await request(app).post("/reset/password").send({ email: "a@b.c", password: "x" });
      expect(res.status).toBe(429);
    });

    Intended? Informational: this is reported so the front door stays in view, not because it is wrong.

    auth-entry-public

  • Lowhigh confidencerole is not connected to anything.

    role is connected to nothing: dead weight, or a route that was meant to use it and does not.

    Missing
    A route that uses it, or its removal.
    Confidence
    Nothing is wired to it in the design.

    Intended? If it is used by a job or another service, accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    no-orphan-datastore

  • Lowhigh confidencestatus is not connected to anything.

    status is connected to nothing: dead weight, or a route that was meant to use it and does not.

    Missing
    A route that uses it, or its removal.
    Confidence
    Nothing is wired to it in the design.

    Intended? If it is used by a job or another service, accept it as known debt: run wyro-check --update-baseline and commit the ledger. It stays visible and CI fails only on new findings.

    no-orphan-datastore

  • Lowlow confidenceGET /:path has no validator or rate limiter attached.

    GET /:path can be called at any rate. Cheap to fix, rarely urgent on a read.

    Missing
    A rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    // 60 requests per IP per minute.
    app.get("/:path", rateLimit({ windowMs: 60_000, limit: 60 }), handler);

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

  • Lowlow confidenceGET / has no validator or rate limiter attached.

    GET / can be called at any rate. Cheap to fix, rarely urgent on a read.

    Missing
    A rate limit.
    Confidence
    Inline validation (zod.parse, pydantic models) and platform rate limits are not always visible to the parser. Check before acting.

    Fix

    // 60 requests per IP per minute.
    app.get("/", rateLimit({ windowMs: 60_000, limit: 60 }), handler);

    Intended? If validation and rate limiting happen upstream (an API gateway, Vercel Firewall), turn the rule off for the repo: { "policy": { "rules": { "public-entry-guarded": "off" } } } in wyro.json.

    public-entry-guarded

Free account, no card. The repository opens as an editable graph.

Add this check to the README

wyro architecture badge
[![wyro architecture](https://wyro.in/api/badge/brocoders/nestjs-boilerplate)](https://wyro.in/scan/brocoders/nestjs-boilerplate)

It updates itself whenever the repository changes and links back to this report.

What this is

Wyro reads the repository’s routes and data models and checks the paths between them: whether a route can reach a table without passing a guard, whether a datastore holding personal data is exposed to a public read, whether an endpoint that issues credentials requires the credentials it issues.

It is rule-based, not a model. The same commit produces the same result every time, and it does not guess at business rules it cannot see. A rule with nothing to look at is reported as not having run — never as a pass.

This check runs on public source through GitHub’s own API and needs no account. A free account adds the editable architecture canvas, private repositories on paid plans, and a CI gate. No card required.

Maintain this repository? You can have this report removed, and a real vulnerability is disclosed to you privately before it is published. How public reports are handled.