Application Security

FastAPI Security Best Practices

Securing FastAPI applications with Pydantic validation, OAuth2 integration, and dependency injection patterns.

Bob
Security Researcher
4 min read

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:

  1. Use strong secrets. Generate with openssl rand -hex 32.
  2. Set short expiration times (15-30 minutes for access tokens).
  3. Always validate the exp claim.
  4. Use HS256 or RS256, never none.
  5. 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.

Never miss an update

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