Python 4 min read

FastAPI Tutorial Part 18: API Security Best Practices

Secure your FastAPI application against common vulnerabilities. Learn input validation, rate limiting, CORS, and OWASP security patterns.

MR

Moshiour Rahman

Advertisement

Introduction

Security is not an afterthought; it’s a fundamental requirement for any production API. In this tutorial, we’ll cover how to harden your FastAPI application using industry-standard practices, effectively mitigating risks like those found in the OWASP Top 10.

We will focus on:

  • Input Validation: preventing injection attacks.
  • Authentication: securing endpoints with JWT.
  • Rate Limiting: preventing DoS attacks.
  • CORS & Headers: browser-side security.

Input Validation

FastAPI’s integration with Pydantic offers a powerful first line of defense. By strictly defining schemas, we reject malformed data before it even reaches our business logic.

Pydantic Validation

Here’s how to enforce strict patterns on user input to prevent common injection attacks (SQLi, XSS):

from pydantic import BaseModel, Field, field_validator
import re

class UserInput(BaseModel):
    # Enforce alphanumeric characters only to prevent script injection
    username: str = Field(min_length=3, max_length=50, pattern=r"^[a-zA-Z0-9_]+$")
    email: str = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")

    @field_validator("username")
    @classmethod
    def no_sql_injection(cls, v):
        # Additional layer of defense (though parameterized queries are better)
        dangerous = ["'", '"', ";", "--", "/*", "*/"]
        if any(char in v for char in dangerous):
            raise ValueError("Invalid characters detected")
        return v

SQL Injection Prevention

Input validation is good, but parameterized queries are non-negotiable. Never concatenate strings to build SQL queries.

# ❌ VULNERABLE: SQL Injection Risk
# query = f"SELECT * FROM users WHERE name = '{user_input}'"

# ✅ SECURE: Parameterized Query
# The database driver handles escaping automatically
result = db.execute(
    text("SELECT * FROM users WHERE name = :name"),
    {"name": user_input}
)

Rate Limiting

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.get("/api/data")
@limiter.limit("10/minute")
async def get_data(request: Request):
    return {"data": "limited"}

Security Headers

from fastapi.middleware.cors import CORSMiddleware

@app.middleware("http")
async def add_security_headers(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["Strict-Transport-Security"] = "max-age=31536000"
    return response

CORS Configuration

Cross-Origin Resource Sharing (CORS) is a browser security feature. If your frontend (e.g., localhost:3000) talks to your backend (localhost:8000), the browser will block it unless you explicitly allow it.

Browser Preflight Check

CORS Preflight Request

Setup

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    # ⚠️ Security Risk: Don't use ["*"] in production!
    allow_origins=["https://yourdomain.com", "https://staging.yourdomain.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Secrets Management

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    secret_key: str  # From environment
    database_url: str
    api_key: str

    class Config:
        env_file = ".env"

# Never hardcode secrets
# Use: export SECRET_KEY="your-secret" or .env file

Authentication Security

Modern APIs typically use Stateless Authentication via JWTs (JSON Web Tokens). This allows your API to scale horizontally without syncing sessions.

JWT Authentication Flow

JWT Authentication Flow

Implementation Details

Password hashing is crucial. We use bcrypt because it is slow by design, making brute-force attacks expensive.

from passlib.context import CryptContext

# Configure hashing algorithm
pwd_context = CryptContext(
    schemes=["bcrypt"],
    deprecated="auto",
    bcrypt__rounds=12  # Higher rounds = more secure but slower
)

# Configuration for JWT
ACCESS_TOKEN_EXPIRE_MINUTES = 15  # Short lifespan reduces risk if stolen
REFRESH_TOKEN_EXPIRE_DAYS = 7     # Usage of refresh tokens is recommended for UX

Rate Limiting

Prevent Abuse and Denial-of-Service (DoS) attacks by limiting how often a user can hit your API. We use slowapi, which implements a “Token Bucket” algorithm.

from slowapi import Limiter
from slowapi.util import get_remote_address

# Identify users by IP address (can also use User ID)
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.get("/api/data")
@limiter.limit("10/minute")  # Allow only 10 requests per minute
async def get_data(request: Request):
    return {"data": "limited", "status": "ok"}

Security Checklist

VulnerabilityPrevention
SQL InjectionParameterized queries
XSSInput validation, output encoding
CSRFCORS, SameSite cookies
Auth bypassJWT validation, secure sessions
Data exposureResponse filtering

Summary

Security LayerImplementation
InputPydantic validation
TransportHTTPS, security headers
AuthenticationJWT, bcrypt
Rate limitingslowapi

Next Steps

In Part 19, we’ll explore OpenAPI and Documentation - creating comprehensive API documentation.

Series Navigation:

  • Part 1-17: Previous parts
  • Part 18: Security (You are here)
  • Part 19: OpenAPI & Documentation

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.