Fix cards for Python and FastAPI
A01:2025 Broken Access Control
- Missing object-level authorization (IDOR and BOLA)CWE-862 · CWE-639 · CWE-863
Build the signed-in user in a dependency (current_user) from the authenticated session and the users table, never from request data, and put the same scope in every query's WHERE clause: same tenant, and the caller's own rows unless they are a billing admin.
- Open redirectCWE-601
Refuse backslashes and control characters, then split next with urlsplit: accept a path that starts with exactly one slash, or an https URL whose hostname is in an exact allowlist, with no port and no @.
- Path traversalCWE-22 · CWE-23 · CWE-36 · CWE-59
Resolve both paths and compare whole components before building the FileResponse: base = Path(base_dir).resolve(), target = (base / name).resolve(), and serve target only if target.is_relative_to(base). resolve() collapses ../ and follows symlinks, and the check also catches absolute names, which replace base in the / operator.
- Server-side request forgery (SSRF)CWE-918
Attach the policy to the connection, not the URL string or its Pydantic type.
A04:2025 Cryptographic Failures
- Weak password hashingCWE-916 · CWE-759 · CWE-327
Use argon2-cffi's PasswordHasher with explicit parameters at or above OWASP's minimum, PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1), and store ph.hash(password) from a plain def path operation.
A05:2025 Injection
- Code injectionCWE-94 · CWE-95
Never evaluate user input as code. Parse the expression with ast.parse and walk the tree, allowing only numeric literals, the supplied variables and arithmetic operators; refuse everything else.
- SQL injectionCWE-89
Pass values as bound parameters: text("... WHERE c.name = :customer") with session.execute(sql, {"customer": customer}), or build the query with select() and column comparisons.
A06:2025 Insecure Design
- Unbounded resource consumptionCWE-770 · CWE-400
Bound every resource the client controls. Cap the request body while you read it -- count the stream and return 413 as soon as it exceeds the limit, never trusting Content-Length -- and bound pagination with Query(limit, ge=1, le=100) so an out-of-range page size is a 422.
- Unrestricted file uploadCWE-434 · CWE-646 · CWE-73 · CWE-770
Decide the type from the bytes and never let the client pick the name.
A07:2025 Authentication Failures
- JWT validation in APIs (bearer tokens)CWE-347 · CWE-287
Put verification in one dependency that every protected route declares: HTTPBearer() reads the token, then jwt.decode(token, key, algorithms=["RS256"], issuer=..., audience=..., leeway=30, options={"require": ["exp", "iss", "aud", "sub"]}) with the key from configuration, and any PyJWTError becomes 401.
- Missing authentication for a critical functionCWE-306
Put an authentication dependency on every protected path operation.
A08:2025 Software or Data Integrity Failures
- Insecure deserializationCWE-502
Do not rebuild objects from client data. Parse the token as JSON with a Pydantic model (Profile.model_validate_json(raw)), which keeps only declared fields of declared types; pickle.loads runs code chosen by the bytes.
- Mass assignment (over-posting)CWE-915
Give each write route its own Pydantic body model that declares only the fields that caller may change (here displayName and bio), never the model that describes the whole stored user, and copy model_dump(exclude_none=True) onto the record. extra="forbid" turns any other key into a 422.
Not covered in FastAPI yet
These weaknesses have fix cards in other stacks but not in this one.
- Cross-site request forgery (CSRF)
- CORS misconfiguration on credentialed APIs
- XML external entity (XXE) processing
- Insecure randomness for security tokens
- Cross-site scripting (XSS)
- CSV formula injection
- LDAP filter injection
- NoSQL operator injection
- OS command and argument injection
- Server-side template injection
- XPath injection
- Clickjacking
- Regular-expression denial of service (ReDoS)
- Hard-coded secrets
- Improper TLS certificate validation
- Session fixation and session cookies
- CRLF injection in logs and response headers
Supported versions
FastAPI has no long-term support lines: fixes ship in new releases, and its documentation tells you to pin the version you test. FastAPI accepts any Starlette from 0.46.0, so pin Starlette yourself. FastAPI and Starlette support policy.
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| FastAPI 0.143 | 0.143.0 | SupportedLatest release; older releases get no fixes. | 8 October 2026 | No date set |
| Starlette 1.x | 1.7.0 | Supported1.0.0 was released on 2026-03-22. | 22 March 2026 | No date set |
| Version | Latest | Status | Released | End of support |
|---|---|---|---|---|
| 3.15 | 3.15.0 | Supported | 9 October 2026 | October 2031 |
| 3.14 | 3.14.8 | Supported | 7 October 2025 | October 2030 |
| 3.13 | 3.13.16 | Security fixes only | 7 October 2024 | October 2029 |
| 3.12 | 3.12.15 | Security fixes only | 2 October 2023 | October 2028 |
| 3.11 | 3.11.17 | Security fixes only | 24 October 2022 | October 2027 |
FastAPI 0.143 still accepts Python 3.10, but Python 3.10 reached its end of life on 2026-10-01, so run 3.11 or later. CPython release schedule
Minimum secure versions
The lowest release in each supported line that fixes every published advisory listed here. Anything older is missing a security fix.
| Package | At least | Why |
|---|---|---|
starlette | 1.3.1 | CVE-2026-54283 (form size limits ignored for URL-encoded bodies), CVE-2026-48818 (SSRF and NTLM credential theft through UNC paths in StaticFiles on Windows), CVE-2026-48710 and others. Advisory |
python-multipart | 0.0.31 | CVE-2026-53539 (quadratic-time parsing, fixed in 0.0.30), CVE-2026-53540 and earlier denial-of-service fixes; FastAPI does not require a fixed version. Advisory |
CPython | 3.15.0, 3.14.8, 3.13.16, 3.12.15 or 3.11.17 | Six CVEs fixed on 30 September and 1 October 2026, such as an ssl use-after-free (CVE-2026-19445), a tarfile filter bypass (CVE-2026-19672) and unbounded zipfile decompression (CVE-2026-15310); 3.11 to 3.13 also fix two more tarfile CVEs, and the fix for one of them, CVE-2026-87910, was in no 3.14 release on 11 October 2026. Advisory |
Middleware and security order
- The last middleware added runs first: Each
app.add_middleware()wraps the app, so the last one added is the outermost. Add them in reverse of the order requests should meet them. - Host and scheme:
TrustedHostMiddlewarewith your host names outermost, thenHTTPSRedirectMiddlewareif the app is reached directly; behind a proxy, set Uvicorn's--forwarded-allow-ipsto the proxy's address. - CORS:
CORSMiddlewarewith a list of origins. - Authentication and authorization as dependencies: Declare the current-user dependency on the router or each route, and check object ownership in the route or its query.
- Exception handlers: Return generic errors for unexpected exceptions; keep
debug=False.
Secure defaults and settings
| Setting | Value | Why |
|---|---|---|
docs_url, redoc_url, openapi_url | /docs, /redoc, /openapi.json | Set them to None in production unless the API is meant to be public. |
| Pydantic extra fields | Ignored | Use separate input models; extra='forbid' makes probing visible with a 422. |
debug | False | Keep it off; debug mode returns tracebacks. |
| Validation errors | 422 with details, including the input | Fine for APIs, but do not echo secrets submitted in bodies. |
Escape hatches to search for
Framework APIs that switch a protection off. Each one is sometimes needed; search for them in review and make each use justify itself.
| API | Risk | Instead |
|---|---|---|
CORSMiddleware(allow_origins=["*"], allow_credentials=True) | Starlette then echoes the request's Origin and allows credentials, so any site can read authenticated responses. | A list of origins. |
Returning ORM objects without response_model | Every column reaches the client, including password hashes and internal flags. | A response model with only the public fields. |
HTMLResponse with an f-string | No escaping. | Jinja2Templates, which autoescapes HTML templates. |
Uvicorn --forwarded-allow-ips='*' | Any client can set its own address and scheme. | The proxy's address. |
SQLAlchemy text() with an f-string | SQL injection. | Bound parameters or the ORM. |
pickle.loads() or yaml.load() on request data | Code execution while deserializing. | Pydantic models over JSON, or yaml.safe_load(). |
Checklist, mapped to ASVS 5.0.0
- v5.0.0-8.2.2 Every route that takes an object ID checks the current user owns it.
- v5.0.0-15.3.1 Every route declares a
response_modelwith only the fields callers may see. - v5.0.0-15.3.3 Request bodies use input models that hold only the editable fields.
- v5.0.0-3.4.2 CORS allows a list of origins, never
*with credentials. - v5.0.0-13.4.5 The interactive docs and OpenAPI schema are off in production unless intended.
- v5.0.0-1.2.4 Queries bind parameters.
- v5.0.0-9.1.1 Bearer tokens are verified (signature, algorithm, issuer, audience, expiry) in a dependency.
- v5.0.0-15.2.1 Starlette and python-multipart are pinned at or above the minimum secure versions above.