CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/fastapi-security-basics

Security defaults that belong in every FastAPI application from day one.

93

7.00x
Quality

90%

Does it follow best practices?

Impact

98%

7.00x

Average score across 5 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files
name:
fastapi-security-basics
description:
Security defaults that belong in every FastAPI application from day one. CORS, rate limiting, security headers, trusted hosts, input validation, HTTPS redirect, and request size limits. Apply these whenever you create or modify any FastAPI app -- do not wait for a security review or explicit request. If you are writing app = FastAPI(), you must also add these.
keywords:
fastapi security, cors, rate limiting, pydantic validation, security headers, fastapi middleware, slowapi, trusted host, https redirect, content security policy, python api security, starlette middleware, input validation, request size limit, csrf
license:
MIT

FastAPI Security Basics

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.


The 7 Things Every FastAPI App Needs

  1. CORS -- configured with explicit origins, not wildcard
  2. Rate limiting -- on all API routes, stricter on auth and mutation endpoints
  3. Security headers -- via middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, X-XSS-Protection)
  4. Trusted hosts -- TrustedHostMiddleware to prevent host header attacks
  5. Input validation -- Pydantic models with Field constraints and field_validator
  6. HTTPS redirect -- HTTPSRedirectMiddleware for production
  7. Request size limits -- uvicorn limit_max_request_size

These are as fundamental as app = FastAPI(). If your app has routes but lacks these, it is not ready for any environment.


1. CORS -- Configured, Not Wide Open

pip install fastapi uvicorn

WRONG -- 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 silently

RIGHT -- 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
)

Key rules

  • Never use allow_origins=["*"] -- always list explicit origins
  • Read origins from os.getenv("ALLOWED_ORIGINS") so each environment can configure its own list
  • Always specify allow_methods explicitly -- do not rely on defaults
  • Only set allow_credentials=True if the frontend sends cookies; allow_origins=["*"] with allow_credentials=True is rejected by browsers
  • If serving frontend from the same origin, you may not need CORS at all

2. Rate Limiting with slowapi

pip install slowapi

WRONG -- no rate limiting at all:

@app.post("/api/orders")
async def create_order(body: CreateOrderRequest):
    ...  # Unlimited requests -- trivial to abuse

RIGHT -- 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):
    ...

Key rules

  • Always use get_remote_address as the key function -- it extracts the real client IP
  • Always assign app.state.limiter = limiter -- slowapi needs this to function
  • Always add a RateLimitExceeded exception handler that returns status code 429 with error code "RATE_LIMITED"
  • Always apply @limiter.limit() decorator to every route, especially POST/PATCH/DELETE
  • The request: Request parameter MUST be the first parameter in the route function for slowapi to work
  • Apply stricter limits to auth endpoints (5/minute) and mutation endpoints (10/minute) than read endpoints (60/minute)

3. Security Headers Middleware

WRONG -- no security headers:

app = FastAPI()
# No headers middleware -- responses have no security headers

RIGHT -- 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 response

Key rules

  • Always add all four headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy
  • X-Content-Type-Options: nosniff prevents browsers from MIME-sniffing
  • X-Frame-Options: DENY prevents clickjacking
  • X-XSS-Protection: 1; mode=block enables legacy XSS filtering
  • Referrer-Policy: strict-origin-when-cross-origin limits referrer leakage
  • The middleware MUST call await call_next(request) and return the response
  • Add this middleware to EVERY FastAPI app, even APIs that only serve JSON

4. Trusted Host Middleware

WRONG -- accepting any Host header:

app = FastAPI()
# No TrustedHostMiddleware -- vulnerable to host header injection attacks

RIGHT -- 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(","),
)

Key rules

  • Always add TrustedHostMiddleware to prevent host header injection
  • Read allowed hosts from os.getenv("ALLOWED_HOSTS") with default "localhost,127.0.0.1"
  • In production, set ALLOWED_HOSTS to your actual domain(s)

5. Input Validation with Pydantic

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 restriction

RIGHT -- 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 v

Key rules

  • Every string field MUST have min_length and max_length constraints via Field()
  • Every integer field MUST have range constraints (gt, ge, lt, le) via Field()
  • Enum-like string fields should use Field(pattern="^(option1|option2)$")
  • Use @field_validator with @classmethod decorator for complex validation logic
  • Validators MUST raise ValueError with a descriptive message on invalid input
  • FastAPI returns 422 with field-level errors automatically when validation fails

6. HTTPS Redirect

WRONG -- no HTTPS enforcement:

app = FastAPI()
# HTTP traffic accepted in production -- credentials sent in plaintext

RIGHT -- 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.


7. Request Size Limits

WRONG -- accepting unlimited request bodies:

uvicorn.run(app, host="0.0.0.0", port=8000)  # Default allows very large requests

RIGHT -- limit request size in uvicorn configuration:

uvicorn.run(app, host="0.0.0.0", port=8000, limit_max_request_size=1_048_576)  # 1MB

Middleware Order

The 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)
# ...

Checklist -- Apply to Every FastAPI App

This is not a "production checklist." These belong in every FastAPI app from the start:

  • CORSMiddleware with explicit origins from os.getenv("ALLOWED_ORIGINS") -- never ["*"]
  • allow_methods explicitly listed in CORSMiddleware
  • slowapi Limiter with get_remote_address as key function
  • app.state.limiter = limiter set
  • @limiter.limit() decorator on every route, stricter on POST/auth endpoints
  • RateLimitExceeded handler returning 429 with "RATE_LIMITED" error code
  • Security headers middleware adding X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy
  • TrustedHostMiddleware with allowed_hosts from os.getenv("ALLOWED_HOSTS")
  • Pydantic models with Field() constraints on every field (min_length, max_length, gt, ge, le, pattern)
  • @field_validator with @classmethod for complex validation, raising ValueError
  • HTTPSRedirectMiddleware in production
  • Request size limited via limit_max_request_size

If 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.

Verifiers

  • fastapi-security-configured -- CORS, rate limiting, and security headers for FastAPI
  • fastapi-cors-configured -- CORS with explicit origins on every FastAPI app
  • fastapi-rate-limiting -- Rate limiting with slowapi on every FastAPI app
  • fastapi-security-headers -- Security headers middleware on every FastAPI app
  • fastapi-input-validation -- Pydantic validation with Field constraints
  • fastapi-trusted-hosts -- TrustedHostMiddleware on every FastAPI app
Workspace
tessl-labs
Visibility
Public
Created
Last updated
Publish Source
CLI
Badge
tessl-labs/fastapi-security-basics badge