Safeguard
Security Guides

FastAPI Security Best Practices: A 2026 Guide

FastAPI's type system catches a whole class of bugs for free, but async I/O, JWT handling, and dependency injection introduce risks that Pydantic will not save you from.

Priya Mehta
Security Researcher
5 min read

FastAPI does something unusual among web frameworks: it turns your type hints into a validation layer, so a surprising amount of input handling is secure before you write a single check. That is real, and it is worth appreciating. But it also lulls teams into thinking the framework has security covered. It does not. The bugs that hit FastAPI apps in production live in authentication logic, async resource handling, and the dependency tree, none of which Pydantic touches.

What the type system does and does not cover

Pydantic validates the shape and type of incoming data. A field declared as EmailStr will reject not-an-email, and an int field with ge=0 will reject negatives. That eliminates a class of malformed-input bugs. What it cannot do is enforce authorization, prevent injection into downstream systems, or decide whether a valid value is one this particular user is allowed to send. Those are semantic questions, and they are entirely on you.

Authentication: the JWT algorithm-confusion trap

The most damaging FastAPI auth bug is not weak secrets; it is accepting the algorithm the token claims to use. If your verification code trusts the alg header, an attacker can switch an RS256 token to HS256 and sign it with your public key, or set alg: none and drop the signature entirely. Always pin the algorithm on the verify call:

from jose import jwt, JWTError
from fastapi import HTTPException

def verify(token: str) -> dict:
    try:
        # Pin the algorithm. Never let the token dictate it.
        return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

Keep access tokens short-lived (15 to 30 minutes), validate the exp claim (jose does this by default), and store refresh tokens in HttpOnly cookies rather than in localStorage, where any XSS can read them.

Authorization belongs in dependencies

FastAPI's dependency injection is the natural home for authorization, and it is worth using well. A reusable dependency keeps the check out of your route bodies where it is easy to forget:

from fastapi import Depends, HTTPException

def require_role(role: str):
    async def checker(user=Depends(get_current_user)):
        if role not in user.roles:
            raise HTTPException(status_code=403, detail="Forbidden")
        return user
    return checker

@app.delete("/projects/{pid}", dependencies=[Depends(require_role("admin"))])
async def delete_project(pid: int):
    ...

Beware the most common authorization gap in REST APIs: broken object-level authorization. Checking that a user is logged in is not the same as checking they own the object at pid. Verify ownership inside the handler, every time.

Async pitfalls that become security bugs

FastAPI is async-first, and async introduces failure modes that synchronous code does not have. Two matter for security. First, blocking calls inside an async route stall the entire event loop, turning a slow downstream call into an accidental denial of service; push blocking work to run_in_threadpool or a real task queue. Second, background tasks run after the response is sent, so an exception there is invisible to the client and easy to ignore:

from fastapi import BackgroundTasks

@app.post("/audit")
async def record(event: Event, tasks: BackgroundTasks):
    # Failures here never reach the client. Log and monitor them,
    # or a dropped audit write becomes a silent compliance gap.
    tasks.add_task(write_audit_log, event)
    return {"status": "accepted"}

Do not ship your schema to attackers

FastAPI auto-generates interactive docs at /docs and a machine-readable schema at /openapi.json. In production this is a reconnaissance gift: every route, parameter, and model laid out for an attacker. Disable it on public deployments or gate it behind authentication:

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

Rate limiting and CORS

FastAPI ships neither rate limiting nor a safe default CORS policy. Add slowapi (backed by Redis in production) to throttle authentication endpoints, and configure CORS with an explicit origin allowlist. The combination allow_origins=["*"] with allow_credentials=True is invalid and signals a misunderstanding of how CORS credentials work.

Because these are runtime behaviors, the reliable way to confirm they hold in your deployed environment is an external probe. A dynamic application security test will tell you whether /openapi.json is actually reachable, whether your CORS policy leaks, and whether an unauthenticated caller can hit a route you thought was protected.

The dependency tree

A FastAPI service depends on starlette, pydantic, uvicorn, and whatever HTTP, database, and auth libraries you add, each with its own transitive tree. Pin with hashes and generate an SBOM so you know exactly what runs:

pip-compile --generate-hashes requirements.in
cyclonedx-py environment --output-format json --outfile sbom.json

Hardening checklist

RiskControl
Algorithm confusionPin algorithms= on every jwt.decode
Broken object authorizationVerify ownership in the handler, not just auth
Event-loop DoSNo blocking calls in async routes
Recon via schemaDisable /docs and /openapi.json in prod
AbuseRate-limit auth endpoints with Redis-backed slowapi
Vulnerable depsHash-pinned, SBOM generated, continuously scanned

How Safeguard helps

We are candid about the boundary: Safeguard does not write your authorization logic, and it cannot know which routes should be public. What it does is own the dependency and remediation layer so that a CVE in starlette or a transitive HTTP client does not sit unnoticed. Our software composition analysis maps the whole tree from the SBOM your pipeline produces, and when a fixed version exists, automated remediation drafts the upgrade PR with reachability context. You can run the same scan against your project before it merges using the Safeguard CLI, so the dependency layer is green before the code ever ships.

Get started

Pin your JWT algorithms and lock down your schema today, then let Safeguard watch the packages. Create a free project at app.safeguard.sh/register and follow the FastAPI setup at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.