Originally published on navspace.dev.
Securing a FastAPI application comes down to six layers: strict Pydantic input validation, hardened JWT authentication, OWASP-aligned protections (SQL injection, mass assignment, broken access control), rate limiting, dependency scanning, and locked-down CORS. FastAPI gives you better security defaults than most frameworks - automatic input validation alone eliminates a whole vulnerability class - but the defaults are not enough for production. This checklist covers the exact configurations that separate a secure FastAPI deployment from the ones that end up in breach postmortems.
Layer 1: Pydantic Validation as Your First Security Boundary
Pydantic models validate every request before your code runs - but only as strictly as you define them. Loose models are the most common FastAPI security gap:
- Use strict types:
EmailStrfor emails,HttpUrlfor URLs,constr(max_length=...)on every string field - unbounded strings invite payload abuse and storage attacks - Set
model_config = ConfigDict(extra="forbid")on input models - otherwise unexpected fields pass through silently, enabling mass-assignment attacks - Never reuse a database model as an input schema - define separate Create/Update/Response models so clients can never set fields like
is_adminorowner_id - Validate business constraints in the model (
Field(gt=0)for amounts, enum types for status fields), not in scattered endpoint logic - Keep Pydantic updated - validation bypass issues in older versions are documented CVEs; pin
pydantic>=2and scan with pip-audit
💡 The separate-response-model rule is the highest-value habit: returning ORM objects directly has leaked password hashes, internal IDs, and feature flags in real production incidents.
response_modelis an allowlist - use it on every endpoint.
Layer 2: JWT Authentication Done Correctly
Most FastAPI tutorials show JWT auth that works but is not production-safe. The production version differs in five specifics:
- Short-lived access tokens (15-30 minutes) with rotating refresh tokens - a leaked access token then has a small blast radius
- Store refresh tokens server-side (Redis) so they can be revoked on logout or compromise; pure-stateless JWT cannot be invalidated
- Sign with a strong secret from a secrets manager (AWS Secrets Manager, not environment files in the repo) and explicitly pin the algorithm - always verify with
algorithms=["HS256"]to block algorithm-confusion attacks - Put roles/permissions in the token, but re-check critical permissions against the database on sensitive operations - tokens go stale
- Return generic 401 errors - never distinguish "user not found" from "wrong password" in responses or timing
Layer 3: The OWASP Top Risks as They Appear in FastAPI
The OWASP API Security Top 10 maps to specific FastAPI patterns. The three that appear most in real Python codebases:
- Broken Object Level Authorization (BOLA): the #1 API vulnerability. Every endpoint that takes an ID must verify the authenticated user owns that resource -
WHERE id = :id AND owner_id = :user_id, not justWHERE id = :id. FastAPI dependencies make this reusable: write oneget_owned_resourcedependency and use it everywhere. - SQL injection: fully prevented by SQLAlchemy parameterized queries - but only if you never build SQL with f-strings. Ban
text(f"...")in code review; even one dynamic ORDER BY built from user input reopens the hole. - Unrestricted resource consumption: add
slowapirate limiting per user/IP, cap request body size at the reverse proxy, set database statement timeouts, and paginate every list endpoint with an enforced maximum page size.
Layer 4: Transport, Headers, and CORS
Configuration-level protections that take minutes and block entire attack classes:
- CORS: list exact origins -
allow_origins=["https://app.example.com"]. Never combineallow_origins=["*"]withallow_credentials=True; that combination hands your API to any website - Force HTTPS with HSTS at the load balancer or reverse proxy; redirect HTTP permanently
- Set security headers (X-Content-Type-Options, X-Frame-Options, a restrictive CSP if the API serves any HTML) via middleware or the proxy
- Hide the interactive docs in production or gate them behind auth -
docs_url=Noneunless your API is intentionally public; Swagger UI is reconnaissance gold - Disable server version banners and debug mode - tracebacks in responses leak file paths and dependency versions
Layer 5: Dependencies and Secrets
Most Python API breaches start in the supply chain or leaked credentials, not in your code:
- Run
pip-audit(or GitHub Dependabot) in CI on every build - fail the pipeline on known CVEs in FastAPI, Pydantic, python-jose, or SQLAlchemy - Pin exact dependency versions in a lock file; review changelogs before upgrading auth-related libraries
- Load secrets from AWS Secrets Manager or SSM at runtime - .env files are for local development only and must be gitignored
- Rotate database credentials and JWT secrets on a schedule and immediately after any team departure
- Scan Docker images (Trivy) - base image CVEs count as your CVEs
Implementation Checklist
- [ ] All input models use strict types, max lengths, and extra="forbid"
- [ ] Every endpoint declares an explicit response_model - no raw ORM objects returned
- [ ] Access tokens expire in ≤30 minutes; refresh tokens are revocable server-side
- [ ] JWT verification pins the algorithm explicitly
- [ ] Every ID-based endpoint checks resource ownership (BOLA protection)
- [ ] Rate limiting active per user and per IP; all list endpoints paginated
- [ ] CORS lists exact origins; docs disabled or gated in production
- [ ] pip-audit and image scanning run in CI; secrets live in a secrets manager
- [ ] Structured logs capture auth failures and permission denials for alerting
Common Mistakes to Avoid
- Returning SQLAlchemy models directly from endpoints — the fastest way to leak password hashes and internal fields.
- Using allow_origins=["*"] with credentials enabled because "CORS errors were annoying" - this disables the browser's core API protection.
- Checking only that a user is authenticated, not that they own the resource - BOLA is the most exploited API vulnerability in the wild.
- Leaving /docs and /openapi.json public on an internal API - attackers get a complete, accurate map of your attack surface.
- Storing JWT secrets in the repository or a committed .env file - leaked git history keeps secrets forever.
FAQ
Is FastAPI secure by default?
FastAPI is more secure by default than most Python frameworks - automatic Pydantic validation blocks malformed input, and its dependency injection makes auth checks reusable and hard to forget. But "secure by default" does not cover production needs: it ships with no rate limiting, no security headers, open docs endpoints, and whatever CORS you configure. The framework prevents injection-class bugs; the deployment-level protections are on you.
How do I protect a FastAPI application against the OWASP Top 10?
Map each risk to a FastAPI mechanism: broken object-level authorization → ownership-checking dependencies on every ID endpoint; injection → SQLAlchemy parameterized queries only, never f-string SQL; broken authentication → short-lived JWTs with pinned algorithms and revocable refresh tokens; unrestricted resource consumption → slowapi rate limiting plus enforced pagination; security misconfiguration → exact-origin CORS, hidden docs, security headers. Pydantic with extra="forbid" additionally blocks mass assignment.
Does Pydantic prevent SQL injection?
Not directly - Pydantic validates shape and types, which narrows the attack surface, but SQL injection prevention comes from parameterized queries in SQLAlchemy (or any DB driver used correctly). The combination is what works: Pydantic rejects structurally invalid input at the boundary, and the ORM ensures whatever passes through is never interpolated into SQL strings. A string field that passes validation can still contain an injection payload; parameterization makes it inert.
How should I rate limit a FastAPI API?
Use slowapi (a FastAPI-compatible port of Flask-Limiter) backed by Redis for multi-instance deployments. Apply tiered limits: strict on auth endpoints (5 attempts/minute per IP to stop credential stuffing), moderate per-user limits on expensive endpoints, and a global per-IP ceiling. Additionally cap request body size at the reverse proxy and set database statement timeouts - rate limiting requests does not help if one request can run a 60-second query.
Should FastAPI docs be disabled in production?
For internal and B2B APIs, yes - set docs_url=None and openapi_url=None, or gate them behind authentication. The interactive docs expose every endpoint, parameter, and schema, which is a complete reconnaissance map. For intentionally public APIs, keeping docs available is fine and often good developer experience - the rule is that documentation exposure should be a deliberate product decision, not a framework default you forgot about.
I'm Navjot Singh - I build Python backends, automation pipelines, and AI integrations for startups and growing businesses. Read the full guide on navspace.dev, or see what I do at navspace.dev.
Top comments (0)