FastAPI gives you the tools to build high-performance APIs, but security is a shared responsibility. While the framework provides a solid foundation, it's up to you to use it correctly to protect your application from common threats.
This guide is a practical checklist covering critical security topics for any FastAPI application, including:
Let's look at how to address each one.
A malicious website tricks an authenticated user into unknowingly submitting a request to your application. This is a classic attack vector known as Cross-Site Request Forgery (CSRF). If your authentication relies solely on browser cookies, your app could be vulnerable to actions like changing a user's email or submitting data without their consent.
FastAPI is an API framework and does not have built-in protection against CSRF in the way a traditional web framework like Django does. This is because modern web applications, especially those with separate frontend and backend (SPAs), typically use token-based authentication (like JWTs) instead of session cookies.
This approach is inherently less vulnerable to CSRF.
Your primary defense against CSRF in a modern API setup is a combination of token authentication and a strict Cross-Origin Resource Sharing (CORS) policy.
Use Token-Based Authentication: Instead of cookies, use a bearer token (like a JWT) sent in the Authorization header. Since the token is not sent automatically by the browser with every request, a malicious site cannot force a user's browser to include it. You can learn how to set this up in the guide to FastAPI authentication with JWT.
Configure CORS Correctly: CORS determines which external domains are allowed to access your API. By restricting this to only your trusted frontend application, you prevent other websites from making requests.
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() # Only allow your frontend domain to make requests. origins = [ "https://your-frontend-app.com", "http://localhost:3000", # For local development ] ALLOW_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"] ALLOW_HEADERS = ["Authorization", "Content-Type", "Accept", "Cookie"] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=ALLOW_METHODS, allow_headers=ALLOW_HEADERS, )
allow_methods: Specifies which HTTP methods are permitted. While ["*"] allows all methods, it is better to list only the ones your frontend application actually uses.allow_headers: Defines which HTTP headers the browser can include in requests. Authorization is necessary for sending authentication tokens, Content-Type is required for sending JSON data, Accept tells the server what content types the client can handle, and Cookie is needed if your application uses cookies.allow_origins: By setting allow_origins to a specific list, you block all other origins from accessing your API. For a more detailed example of how to configure CORS when serving a Single Page Application (SPA), see the guide on serving a React frontend with FastAPI. I have also discussed CORS in my middleware guide: Adding Middlewares in FastAPI.An attacker injects malicious scripts into your application through user input. This attack, known as Cross-Site Scripting (XSS), can lead to the script executing in the browsers of other users, potentially stealing their data or performing actions on their behalf.
FastAPI primarily deals with data, not rendering. It usually returns JSON responses, so the risk of XSS is not on the server but on the frontend that consumes the JSON and renders it as HTML.
The responsibility for preventing XSS lies with the client-side application (e.g., React, Vue). The frontend must treat all data received from the API as untrusted and sanitize it before rendering.
If you use FastAPI to render HTML directly with a templating engine like Jinja2, then the same rules as other web frameworks apply.
Enable Auto-Escaping: Jinja2 auto-escapes all variables by default, which converts characters like < and > into their HTML-safe equivalents (< and >). This prevents injected scripts from running.
Avoid Using |safe: Only use the safe filter if you are absolutely certain the content is from a trusted source and needs to be rendered as raw HTML.
An attacker manipulates application inputs to alter the structure of your SQL queries. A successful SQL Injection can be used to bypass authentication, steal sensitive data, or even delete your entire database.
This vulnerability depends entirely on how you write your database queries. The best defense is to use an Object-Relational Mapper (ORM) like SQLModel or SQLAlchemy.
Always use an ORM, which automatically parameterizes your queries and escapes inputs.
# Safe: Using an ORM (SQLModel example) from sqlmodel import Session, select def get_user(db: Session, username: str): # The ORM handles sanitization. This is safe. statement = select(User).where(User.username == username) user = db.exec(statement).first() return user
If you must write raw SQL, never use string formatting (like f-strings) to insert variables into your query. Use the database driver's parameter substitution instead.
import databases database = databases.Database("postgresql://user:pass@host/db") async def get_user_raw(username: str): # ❌ Unsafe: Vulnerable to SQL injection query_unsafe = f"SELECT * FROM users WHERE username = '{username}'" # ✅ Safe: Uses parameterized queries query_safe = "SELECT * FROM users WHERE username = :username" user = await database.fetch_one(query=query_safe, values={"username": username}) return user
Without rate limiting, an attacker can make an unlimited number of requests to your API. This can be used for brute-force attacks on login endpoints, API abuse, or to overwhelm your server, causing a Denial of Service (DoS).
Use a library like throttled to enforce request limits. It can be applied globally as a dependency to your application.
First, install the necessary packages:
pip install throttled redis
Then, configure and apply the limiters as dependencies to your FastAPI app. This example sets a global limit using in-memory storage and a stricter per-IP limit using Redis.
from fastapi import Depends, FastAPI from redis import Redis from throttled.fastapi import IPLimiter, TotalLimiter from throttled.models import Rate from throttled.storage.memory import MemoryStorage from throttled.storage.redis import RedisStorage # ---- create storages ---- # Use in-memory storage for a simple global limit memory_storage = MemoryStorage() # Use Redis for limits that need to be shared across multiple API instances redis_storage = RedisStorage(client=Redis.from_url("redis://localhost:6379")) # ---- create limiters ---- # 100 req/min global limit total_limiter = TotalLimiter(limit=Rate(100, 60), storage=memory_storage) # 10 req/min per IP limit, shared across all instances via Redis ip_limiter = IPLimiter(limit=Rate(10, 60), storage=redis_storage) # ---- FastAPI app with dependencies ---- app = FastAPI( title="ThrottledAPI Example", dependencies=[Depends(total_limiter), Depends(ip_limiter)], ) @app.post("/login") async def login(): # ... your login logic ... return {"status": "logged in"} @app.get("/data") async def get_data(): return {"data": "some information"}
Beyond CORS, several other HTTP headers instruct browsers on how to behave securely when handling your site's content. These headers can mitigate attacks like clickjacking and XSS. You can add them using a custom middleware.
from fastapi import FastAPI, Request from starlette.middleware.base import BaseHTTPMiddleware class SecurityHeadersMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): response = await call_next(request) response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none';" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response app = FastAPI() app.add_middleware(SecurityHeadersMiddleware)
Strict-Transport-Security (HSTS): Enforces the use of HTTPS.X-Content-Type-Options: Prevents the browser from MIME-sniffing a response away from the declared content type.X-Frame-Options: Protects against clickjacking by preventing your content from being embedded in iframes on other sites.Content-Security-Policy (CSP): Provides granular control over which resources (scripts, styles, images) can be loaded, reducing the risk of XSS.Referrer-Policy: Controls how much referrer information is sent with requests.Authentication confirms a user's identity, but authorization determines their permissions. You should protect sensitive endpoints by checking for specific roles or scopes. This can be done cleanly using another dependency.
Assume your JWT contains a scopes claim. You can create a dependency that requires a specific scope for an endpoint.
from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer # This would be part of your authentication logic oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") def require_scope(required_scope: str): def check_scope(token: str = Depends(oauth2_scheme)): # In a real app, you would decode the JWT and get the scopes # For this example, we'll simulate it. user_scopes = ["read:items", "write:items"] # Decoded from token if required_scope not in user_scopes: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not enough permissions", ) return check_scope # Protect an endpoint with the required scope @app.post("/items", dependencies=[Depends(require_scope("write:items"))]) async def create_item(): # This code will only run if the user's token has the 'write:items' scope return {"status": "item created"}
This pattern ensures that even authenticated users cannot access endpoints they are not authorized for.
pydantic-settings to load them from environment variables and a secret management service like Google Secret Manager.Path(..., pattern="^[\w-]*$") to ensure an ID contains only alphanumeric characters, preventing path traversal (../) or other injection attacks, a technique demonstrated in the file uploads guide.debug=False when initializing your FastAPI app for production to prevent leaking sensitive configuration details in error messages.pip-audit./docs and /redoc are useful for development but can be a security risk in production. Consider disabling them or restricting access using authentication.1. How does Pydantic validation specifically improve security?
Pydantic enforces a strict data contract. Beyond preventing type errors, you can use it to define constraints that block invalid data before it reaches your application logic. For example, you can limit string lengths (max_length), enforce formats (pattern), and set numeric ranges (gt, lt). This reduces the attack surface by ensuring that only well-formed data is processed.
2. Does FastAPI handle authentication?
No, FastAPI does not include a built-in authentication system. However, it provides a robust dependency injection system and security utilities (fastapi.security) that make it straightforward to integrate any authentication scheme. For a detailed walkthrough of implementing OAuth2 with JWT tokens, see the dedicated guide on authentication in FastAPI.
3. How should I handle file uploads securely? File uploads are a significant risk. Do not trust user-provided filenames or content types.
python-magic to verify the file type by inspecting its actual content (magic numbers), not just its extension.uuid.uuid4()) for every uploaded file to prevent path traversal attacks and overwrites.For a detailed walkthrough of these techniques, see the dedicated guide on handling file uploads in FastAPI.
4. How can I protect against large request bodies causing a DoS attack?
You can limit the maximum request body size at the web server level. For example, if you are using a reverse proxy like Nginx, you can set the client_max_body_size directive. For Gunicorn, you can use the --limit-request-body option, a topic covered in the guide to performance tuning on Cloud Run.
5. Should I use a Web Application Firewall (WAF)? A Web Application Firewall (WAF) can provide an additional layer of defense by filtering malicious traffic before it even reaches your application. It's a good practice for production applications, as it can block common attacks like SQL injection and XSS at the edge.
About the Author
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Related Blog Posts
Enjoyed this blog post? Check out these related posts!

6 Essential FastAPI Middlewares for Production-Ready Apps
A guide to the 6 key middlewares for building secure, performant, and resilient FastAPI applications.
Read More...

How to Protect Your FastAPI OpenAPI/Swagger Docs with Authentication
A Guide to Securing Your API Documentation with Authentication
Read More...

How to Handle File Uploads in FastAPI
A Practical Guide to Streaming and Validating File Uploads
Read More...

How to Create and Secure PDFs in Python with FastAPI
A Guide to Generating and Encrypting PDFs with WeasyPrint, pypdf, and FastAPI
Read More...
On this page
Back to Blogs
Contact Me
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.