How do I secure a FastAPI app?

Direct answer

Pydantic handles input validation well, so FastAPI's weak points are elsewhere: CORS set to a wildcard, interactive docs left enabled in production, dependency-injected auth that confirms identity without checking ownership, and raw SQLAlchemy queries bypassing the ORM.

Muhammad HasanUpdated

FastAPI validates request bodies through Pydantic by default, which removes a large class of problems before you write anything. What's left is mostly configuration and authorisation.

CORS

# Vulnerable
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
)

This combination came up a lot across the 24 builds we scanned. Set during development so something would render, then shipped as-is.

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourapp.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
)

Browsers reject wildcard origins combined with credentials, so this often surfaces as a bug rather than a silent risk. Not always, though, and not for non-browser clients.

The docs

/docs and /redoc are enabled by default and describe every endpoint, parameter and schema you have. That's a map of your API.

app = FastAPI(
    docs_url=None if PRODUCTION else "/docs",
    redoc_url=None if PRODUCTION else "/redoc",
    openapi_url=None if PRODUCTION else "/openapi.json",
)

Disabling docs_url alone leaves /openapi.json serving the same information. Disable all three.

Auth dependencies check identity, not ownership

# Vulnerable
@app.get("/orders/{order_id}")
async def get_order(order_id: int, user = Depends(current_user)):
    return db.query(Order).filter(Order.id == order_id).first()

Depends(current_user) confirms someone is authenticated. The query then returns whatever ID was asked for.

# Fixed
order = db.query(Order).filter(
    Order.id == order_id,
    Order.user_id == user.id
).first()
if not order:
    raise HTTPException(status_code=404)

This was the most consequential pattern we found in that study. It's easy to miss in FastAPI specifically, because the dependency injection makes the endpoint look well-protected.

Response models

Returning an ORM object serialises whatever is on it, including columns you didn't intend to expose.

class UserOut(BaseModel):
    id: int
    email: str
    model_config = {"from_attributes": True}

@app.get("/me", response_model=UserOut)
async def me(user = Depends(current_user)):
    return user          # only id and email are serialised

response_model filters the output to declared fields. Without it, a password hash on the model goes out with the response.

Raw SQL

SQLAlchemy's query builder parameterises. text() with an f-string doesn't:

# Vulnerable
db.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))

# Safe
db.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})

The rest

Rate limiting isn't built in. slowapi or a reverse proxy. Set debug=False in production. Bound pagination parameters with Query(le=100) or someone will request every row. Security headers belong in middleware or the proxy in front.

Go deeper

Kolega for AppSec teams

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free