Security defaults that belong in every FastAPI application from day one.
93
90%
Does it follow best practices?
Impact
98%
7.00xAverage score across 5 eval scenarios
Passed
No known issues
Every FastAPI app needs these security defaults from day one -- do not wait for a security review to add them. These are not optional hardening steps; they are baseline requirements for any FastAPI application.
When to apply this skill: Every time you create a new FastAPI application, add routes to an existing one, or refactor a FastAPI project. Even if the task only asks you to "build a REST API" or "create CRUD endpoints," you MUST include all of these. A FastAPI app without them is incomplete.
These are as fundamental as app = FastAPI(). If your app has routes but lacks these, it is not ready for any environment.
pip install fastapi uvicornWRONG -- allows any origin:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Dangerous: allows any website to make requests
)WRONG -- no CORS middleware at all:
app = FastAPI()
# No CORSMiddleware added -- browsers block cross-origin requests silentlyRIGHT -- explicit allowed origins from environment variable:
import os
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(","),
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
allow_credentials=True, # Only if using cookies/sessions
)allow_origins=["*"] -- always list explicit originsos.getenv("ALLOWED_ORIGINS") so each environment can configure its own listallow_methods explicitly -- do not rely on defaultsallow_credentials=True if the frontend sends cookies; allow_origins=["*"] with allow_credentials=True is rejected by browserspip install slowapiWRONG -- no rate limiting at all:
@app.post("/api/orders")
async def create_order(body: CreateOrderRequest):
... # Unlimited requests -- trivial to abuseRIGHT -- slowapi with get_remote_address, rate limit decorator, and exception handler:
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import Request
from fastapi.responses import JSONResponse
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"error": {"code": "RATE_LIMITED", "message": "Too many requests, try again later"}},
)
# General API rate limit
@app.get("/api/items")
@limiter.limit("60/minute")
async def list_items(request: Request):
...
# Stricter limit on mutation endpoints
@app.post("/api/orders")
@limiter.limit("10/minute")
async def create_order(request: Request, body: CreateOrderRequest):
...
# Strictest limit on auth endpoints (prevent brute force)
@app.post("/api/auth/login")
@limiter.limit("5/minute")
async def login(request: Request, body: LoginRequest):
...get_remote_address as the key function -- it extracts the real client IPapp.state.limiter = limiter -- slowapi needs this to functionRateLimitExceeded exception handler that returns status code 429 with error code "RATE_LIMITED"@limiter.limit() decorator to every route, especially POST/PATCH/DELETErequest: Request parameter MUST be the first parameter in the route function for slowapi to workWRONG -- no security headers:
app = FastAPI()
# No headers middleware -- responses have no security headersRIGHT -- custom middleware that adds all four security headers:
from fastapi import Request
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return responseX-Content-Type-Options: nosniff prevents browsers from MIME-sniffingX-Frame-Options: DENY prevents clickjackingX-XSS-Protection: 1; mode=block enables legacy XSS filteringReferrer-Policy: strict-origin-when-cross-origin limits referrer leakageawait call_next(request) and return the responseWRONG -- accepting any Host header:
app = FastAPI()
# No TrustedHostMiddleware -- vulnerable to host header injection attacksRIGHT -- TrustedHostMiddleware with hosts from environment variable:
import os
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(","),
)os.getenv("ALLOWED_HOSTS") with default "localhost,127.0.0.1"ALLOWED_HOSTS to your actual domain(s)FastAPI + Pydantic handles validation automatically, but you MUST add constraints to every field.
WRONG -- no constraints on fields:
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
name: str # Accepts empty string, 10MB string, anything
email: str # No format validation
age: int # Accepts negative numbers, zero
role: str # No enum restrictionRIGHT -- Field constraints plus field_validator for complex validation:
from pydantic import BaseModel, Field, field_validator
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str = Field(min_length=5, max_length=254)
age: int = Field(gt=0, le=150)
role: str = Field(pattern="^(admin|user|moderator)$")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v or "." not in v.split("@")[-1]:
raise ValueError("Invalid email format")
return v.lower()
class OrderItemRequest(BaseModel):
product_id: int = Field(gt=0)
quantity: int = Field(ge=1, le=100)
size: str = Field(pattern="^(small|medium|large)$")
@field_validator("size")
@classmethod
def validate_size(cls, v: str) -> str:
if v not in ("small", "medium", "large"):
raise ValueError("size must be small, medium, or large")
return vmin_length and max_length constraints via Field()gt, ge, lt, le) via Field()Field(pattern="^(option1|option2)$")@field_validator with @classmethod decorator for complex validation logicValueError with a descriptive message on invalid inputWRONG -- no HTTPS enforcement:
app = FastAPI()
# HTTP traffic accepted in production -- credentials sent in plaintextRIGHT -- HTTPSRedirectMiddleware for production:
import os
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
if os.getenv("ENVIRONMENT", "development") == "production":
app.add_middleware(HTTPSRedirectMiddleware)Only enable in production -- HTTPS redirect breaks local development without TLS certificates.
WRONG -- accepting unlimited request bodies:
uvicorn.run(app, host="0.0.0.0", port=8000) # Default allows very large requestsRIGHT -- limit request size in uvicorn configuration:
uvicorn.run(app, host="0.0.0.0", port=8000, limit_max_request_size=1_048_576) # 1MBThe order middleware is registered matters. Security middleware MUST come first.
import os
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi.responses import JSONResponse
app = FastAPI()
# 1. HTTPS redirect (production only)
if os.getenv("ENVIRONMENT") == "production":
app.add_middleware(HTTPSRedirectMiddleware)
# 2. Trusted hosts
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=os.getenv("ALLOWED_HOSTS", "localhost,127.0.0.1").split(","),
)
# 3. CORS
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(","),
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
allow_credentials=True,
)
# 4. Security headers
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
# 5. Rate limiting
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"error": {"code": "RATE_LIMITED", "message": "Too many requests, try again later"}},
)
# 6. Routes (with rate limiting and validated models)
# ...This is not a "production checklist." These belong in every FastAPI app from the start:
os.getenv("ALLOWED_ORIGINS") -- never ["*"]allow_methods explicitly listed in CORSMiddlewareget_remote_address as key functionapp.state.limiter = limiter set@limiter.limit() decorator on every route, stricter on POST/auth endpoints"RATE_LIMITED" error codeallowed_hosts from os.getenv("ALLOWED_HOSTS")Field() constraints on every field (min_length, max_length, gt, ge, le, pattern)@field_validator with @classmethod for complex validation, raising ValueErrorlimit_max_request_sizeIf the task says "build a REST API" or "create CRUD endpoints" and does not mention security, you still add all of the above. Security middleware is not a feature request -- it is part of building a FastAPI app correctly.