Home

Blog

A Practical Guide to Securing FastAPI Applications

10 min read
Browser window with shield and padlock icons representing API security
By David Muraya • October 24, 2025

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:

  • Handling common web vulnerabilities like CSRF, XSS, and SQL Injection.
  • Implementing rate limiting to prevent abuse.
  • Enforcing secure HTTP headers and authorization.
  • Other essential production practices like secret management, secure logging, and protecting API documentation.

Let's look at how to address each one.

1. Cross-Site Request Forgery (CSRF)

The Problem

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's Approach

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.

What You Should Do

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.

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

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

2. Cross-Site Scripting (XSS)

The Problem

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's Approach

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.

What You Should Do

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 (&lt; and &gt;). 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.

3. SQL Injection

The Problem

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.

FastAPI's Approach

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.

What You Should Do

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

4. Rate Limiting

The Problem

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

What You Should Do

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"}

5. Enforce Secure HTTP Headers

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.

6. Implement Authorization and Access Control

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.

Other Essential Security Practices

  • Use HTTPS: In production, always run your application behind a reverse proxy (like Nginx or Caddy) that enforces HTTPS.
  • Manage Secrets Securely: Never hardcode API keys, passwords, or other secrets in your code. Use a library like pydantic-settings to load them from environment variables and a secret management service like Google Secret Manager.
  • Validate All Input: Use Pydantic's powerful type hinting to strictly validate all incoming data. This includes not just request bodies but also path and query parameters. For example, use 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.
  • Disable Debug Mode in Production: Set debug=False when initializing your FastAPI app for production to prevent leaking sensitive configuration details in error messages.
  • Sanitize Error Responses: Ensure that production error responses are generic and do not leak sensitive information like stack traces, database query errors, or internal variable names. Create custom exception handlers to log the full error internally while returning a sanitized message to the client.
  • Implement Security Logging: Your logs are critical for detecting and responding to security incidents. Log security-relevant events such as failed login attempts, access denials (403 errors), changes to user permissions, and any validation errors that could indicate an attempt to probe your API. I have discussed logging in detail in my FastAPI logging guide.
  • Audit Dependencies: Regularly scan your dependencies for known vulnerabilities using a tool like pip-audit.
  • Protect API Documentation in Production: The interactive API docs at /docs and /redoc are useful for development but can be a security risk in production. Consider disabling them or restricting access using authentication.

FAQ

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.

  • Validate File Content: Use a library like python-magic to verify the file type by inspecting its actual content (magic numbers), not just its extension.
  • Use a Secure Filename: Generate a new, random filename (e.g., using uuid.uuid4()) for every uploaded file to prevent path traversal attacks and overwrites.
  • Isolate Uploads: Store uploaded files in a dedicated, non-executable directory outside of your main application root, or preferably in a separate cloud storage bucket.

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.

Share This Article

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

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

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

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

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.

© 2025 David Muraya. All rights reserved.