kunalkapadia/express-mongoose-es6-rest-api
No critical or high-risk findings. 9 worth a look.
13 warnings in the paths between 8 routes and 1 tables.
- ROUTES
- 8
- TABLES
- 1
- FILES READ
- 12/12
- RULES RUN
- 12/12
13 findings
9 medium4 low
POST / makes calls the check could not follow, so its data access is unknown, not absent: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list. It does not declare authentication.
No rule could run on POST /: it calls into code the check could not follow, so whether it reaches data — and whether that is authenticated — is unknown. A route nobody can trace is where a rescue gets hurt.
- Missing
- Evidence of what it touches. Could not follow: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list.
- Confidence
- The check is certain it could not follow these calls; it is not claiming the route is wrong.
Intended? Trace the chain by hand or log at the data access and hit the route. If it touches no data, record it in the baseline; if it does, that is the path to review first.
unresolved-data-reachGET / makes calls the check could not follow, so its data access is unknown, not absent: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list. It does not declare authentication.
No rule could run on GET /: it calls into code the check could not follow, so whether it reaches data — and whether that is authenticated — is unknown. A route nobody can trace is where a rescue gets hurt.
- Missing
- Evidence of what it touches. Could not follow: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list.
- Confidence
- The check is certain it could not follow these calls; it is not claiming the route is wrong.
Intended? Trace the chain by hand or log at the data access and hit the route. If it touches no data, record it in the baseline; if it does, that is the path to review first.
unresolved-data-reachDELETE /:userId makes calls the check could not follow, so its data access is unknown, not absent: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list. It does not declare authentication.
No rule could run on DELETE /:userId: it calls into code the check could not follow, so whether it reaches data — and whether that is authenticated — is unknown. A route nobody can trace is where a rescue gets hurt.
- Missing
- Evidence of what it touches. Could not follow: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list.
- Confidence
- The check is certain it could not follow these calls; it is not claiming the route is wrong.
Intended? Trace the chain by hand or log at the data access and hit the route. If it touches no data, record it in the baseline; if it does, that is the path to review first.
unresolved-data-reachPUT /:userId makes calls the check could not follow, so its data access is unknown, not absent: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list. It does not declare authentication.
No rule could run on PUT /:userId: it calls into code the check could not follow, so whether it reaches data — and whether that is authenticated — is unknown. A route nobody can trace is where a rescue gets hurt.
- Missing
- Evidence of what it touches. Could not follow: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list.
- Confidence
- The check is certain it could not follow these calls; it is not claiming the route is wrong.
Intended? Trace the chain by hand or log at the data access and hit the route. If it touches no data, record it in the baseline; if it does, that is the path to review first.
unresolved-data-reachGET /:userId makes calls the check could not follow, so its data access is unknown, not absent: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list. It does not declare authentication.
No rule could run on GET /:userId: it calls into code the check could not follow, so whether it reaches data — and whether that is authenticated — is unknown. A route nobody can trace is where a rescue gets hurt.
- Missing
- Evidence of what it touches. Could not follow: User.get() — server/user/user.model.js exports no get; User.list() — server/user/user.model.js exports no list.
- Confidence
- The check is certain it could not follow these calls; it is not claiming the route is wrong.
Intended? Trace the chain by hand or log at the data access and hit the route. If it touches no data, record it in the baseline; if it does, that is the path to review first.
unresolved-data-reachPOST /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-guardedPOST / has no validator or rate limiter attached.
POST / 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/' \ -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 / rejects an unexpected body", async () => { const res = await request(app).post("/").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-guardedDELETE /:userId has no validator or rate limiter attached.
DELETE /:userId 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 DELETE 'https://YOUR_API/1' \ -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("DELETE /:userId rejects an unexpected body", async () => { const res = await request(app).delete("/1").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-guardedPUT /:userId has no validator or rate limiter attached.
PUT /:userId 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 PUT 'https://YOUR_API/1' \ -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("PUT /:userId rejects an unexpected body", async () => { const res = await request(app).put("/1").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-guardedusers is not connected to anything.
users 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-baselineand commit the ledger. It stays visible and CI fails only on new findings.no-orphan-datastoreGET /health-check has no validator or rate limiter attached.
GET /health-check can be called at any rate. Cheap to fix, rarely urgent on a read.
- Where
index.route.js:10- 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("/health-check", 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-guardedGET / 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-guardedGET /:userId has no validator or rate limiter attached.
GET /:userId 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("/:userId", 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
[](https://wyro.in/scan/kunalkapadia/express-mongoose-es6-rest-api)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.