fastapi/full-stack-fastapi-template/backend
1 critical finding to fix first.
1 error and 3 warnings in the paths between 23 routes and 2 tables.
- ROUTES
- 23
- TABLES
- 2
- FILES READ
- 19/19
- RULES RUN
- 11/11
4 findings
1 critical2 medium1 low
POST /private/users can reach user without authenticating.
Anyone, signed in or not, can write to user (email, hashed_password) — personal or secret fields.
- Data reached
user(write) · sensitive:email, hashed_password- Missing
- Authentication before the handler reaches data, and a query scoped to the caller.
- Confidence
- No middleware, guard or wrapper is applied to this route anywhere in backend/app/api/routes/private.py.
- Path
- POST /private/users → user
Prove it
# No cookie, no Authorization header. curl -i -X POST 'https://YOUR_API/private/users' \ -H 'content-type: application/json' -d '{}' # Expect 401. A 2xx means anyone on the internet can do this.Fix
@app.post("/private/users") def handler(user: User = Depends(get_current_user)): # get_current_user raises HTTPException(401) when the token is missing ...Regression test
def test_post_requires_auth(client): res = client.post("/private/users") assert res.status_code == 401Intended? This cannot be declared intentional: a public write is not a design choice the check will bless. Accept it as known debt: run
wyro-check --update-baselineand commit the ledger. It stays visible and CI fails only on new findings.auth-before-dataPOST /login/access-token has no validator or rate limiter attached.
POST /login/access-token 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/access-token' \ -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}' # Expect 422. A 2xx or 500 means the body is not validated.Fix
class Body(BaseModel): name: constr(max_length=200) @app.post("/login/access-token") def handler(body: Body): ...Regression test
def test_post_rejects_unexpected_body(client): res = client.post("/login/access-token", json={"unexpected": True}) assert res.status_code == 422Intended? 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 /password-recovery/{email} has no validator or rate limiter attached.
POST /password-recovery/{email} 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/password-recovery/1' \ -H 'content-type: application/json' -d '{"unexpected": {"nested": [1,2,3]}}' # Expect 422. A 2xx or 500 means the body is not validated.Fix
class Body(BaseModel): name: constr(max_length=200) @app.post("/password-recovery/{email}") def handler(body: Body): ...Regression test
def test_post_rejects_unexpected_body(client): res = client.post("/password-recovery/1", json={"unexpected": True}) assert res.status_code == 422Intended? 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 /utils/health-check has no validator or rate limiter attached.
GET /utils/health-check 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
from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) @app.get("/utils/health-check") @limiter.limit("60/minute") def handler(request: Request): ...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/fastapi/full-stack-fastapi-template?path=backend)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.