FastAPI has become the go-to Python framework for building APIs. Its type-hint-driven approach with Pydantic validation catches many security issues at the framework level. But FastAPI is a framework, not a security solution. You still need to make deliberate choices about authentication, authorization, input handling, and deployment.
Pydantic: Validation as a Security Feature
FastAPI uses Pydantic models for request validation. This is a genuine security advantage. Every endpoint with a Pydantic model automatically rejects malformed input:
from pydantic import BaseModel, EmailStr, Field
class CreateUser(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100, pattern=r'^[a-zA-Z\s]+$')
role: Literal['user', 'editor']
age: int = Field(ge=0, le=150)
@app.post("/users")
async def create_user(user: CreateUser):
# Input is already validated by the time we reach this line
return await save_user(user)
If the request body does not match the schema, FastAPI returns a 422 error with details about what failed. No manual validation needed.
Important: Pydantic validation only covers the shape and types of data. It does not check for business logic violations, authorization issues, or semantic attacks. A valid email address can still be used for account enumeration.
Strict Mode
Pydantic v2 supports strict mode, which disables type coercion:
class StrictInput(BaseModel):
model_config = ConfigDict(strict=True)
count: int # "5" (string) will be REJECTED, only 5 (int) accepted
active: bool # "true" (string) will be REJECTED
Use strict mode for security-sensitive endpoints to prevent type confusion attacks.
Authentication
OAuth2 with JWT
FastAPI has built-in OAuth2 support:
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
user_id: str = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = await get_user(user_id)
if user is None:
raise HTTPException(status_code=401, detail="User not found")
return user
JWT Security Rules:
- Use strong secrets. Generate with
openssl rand -hex 32. - Set short expiration times (15-30 minutes for access tokens).
- Always validate the
expclaim. - Use
HS256orRS256, nevernone. - Store refresh tokens in httpOnly cookies, not localStorage.
API Key Authentication
For service-to-service communication:
from fastapi.security import APIKeyHeader
api_key_header = APIKeyHeader(name="X-API-Key")
async def verify_api_key(api_key: str = Security(api_key_header)):
if not secrets.compare_digest(api_key, EXPECTED_API_KEY):
raise HTTPException(status_code=403, detail="Invalid API key")
return api_key
Use secrets.compare_digest() for constant-time comparison to prevent timing attacks.
Authorization
FastAPI's dependency injection is perfect for authorization:
class RoleChecker:
def __init__(self, required_role: str):
self.required_role = required_role
async def __call__(self, user: User = Depends(get_current_user)):
if user.role != self.required_role:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return user
admin_only = RoleChecker("admin")
@app.delete("/users/{user_id}", dependencies=[Depends(admin_only)])
async def delete_user(user_id: int):
...
Rate Limiting
FastAPI does not include rate limiting. Use slowapi:
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/auth/login")
@limiter.limit("5/minute")
async def login(request: Request, credentials: LoginCredentials):
...
For production, use a Redis-backed rate limiter or handle rate limiting at the reverse proxy level.
CORS
Configure CORS explicitly:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
Never use allow_origins=["*"] with allow_credentials=True. Browsers will reject this, but it signals a misunderstanding of CORS.
SQL Injection
Even with Pydantic validation, use parameterized queries:
# Safe: parameterized with SQLAlchemy
result = await db.execute(select(User).where(User.id == user_id))
# Safe: parameterized with async databases library
query = "SELECT * FROM users WHERE id = :id"
result = await database.fetch_one(query, values={"id": user_id})
# DANGEROUS: string formatting
query = f"SELECT * FROM users WHERE id = {user_id}"
Production Deployment
ASGI Server
Run FastAPI behind Uvicorn with production settings:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --proxy-headers --forwarded-allow-ips='*'
Use --proxy-headers when behind a reverse proxy so that FastAPI sees the correct client IP.
Error Handling
Do not expose internal details in production errors:
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled error: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)
Dependency Security
FastAPI projects use pip. Pin dependencies with pip-compile --generate-hashes and audit with pip-audit. Generate SBOMs:
cyclonedx-py environment --output sbom.json
How Safeguard.sh Helps
Safeguard.sh monitors your FastAPI application dependencies continuously. It ingests SBOMs from your CI pipeline, tracks package versions across your services, and alerts you when vulnerabilities are discovered in your Python dependency tree. For teams running multiple FastAPI services, Safeguard.sh provides the cross-service visibility to ensure a CVE in a shared dependency like pydantic or uvicorn is remediated everywhere it appears.