Starlette CSRF Middleware
Starlette-CSRF is an active Python middleware designed for Starlette and FastAPI applications to mitigate Cross-Site Request Forgery (CSRF) attacks. It implements the Double Submit Cookie technique, providing protection by requiring a secret value to be sent in both a cookie and a request header for unsafe HTTP methods. The library is currently at version 3.0.0 and maintains a steady release cadence, with the latest major update focusing on Python version compatibility and argument handling.
Common errors
-
403 Forbidden error on POST requests (or other unsafe methods).
cause The CSRF token expected in the 'x-csrftoken' header (or the configured header name) does not match the token in the 'csrftoken' cookie, or the header is missing entirely.fixEnsure your frontend (HTML form or JavaScript AJAX request) correctly retrieves the `csrftoken` cookie value and sends it in the `x-csrftoken` HTTP header for all unsafe requests (POST, PUT, DELETE, PATCH). For HTML forms, you can inject `request.state.csrftoken` into a hidden input field. For AJAX, read the cookie and set the header manually. -
WebSocket connections fail with `AssertionError` or other unexpected errors when `CSRFMiddleware` is applied globally.
cause The `CSRFMiddleware` is designed for HTTP requests and may not handle WebSocket scopes correctly, leading to internal errors if it processes a WebSocket connection.fixExempt WebSocket routes from CSRF protection by adding them to the `exempt_urls` argument of the `CSRFMiddleware` using a regular expression pattern. Alternatively, wrap the middleware to conditionally apply it only to HTTP scopes, as suggested in some community discussions.
Warnings
- breaking Version 3.0.0 dropped support for Python 3.7. Users on older Python versions must upgrade or remain on `starlette-csrf<3.0.0`.
- breaking Starting with version 2.0.0, middleware initializer arguments (other than `app` and `secret`) became keyword-only. While this primarily affects direct instantiation, it's a breaking change for custom middleware setups.
- gotcha Version 1.4.4 rewrote the middleware as a pure ASGI middleware, moving away from `starlette.middleware.base.BaseHTTPMiddleware` (which is now deprecated in Starlette). While an improvement, this might affect advanced users who previously extended `CSRFMiddleware` and relied on `BaseHTTPMiddleware`'s internal structure or methods.
Install
-
pip install starlette-csrf
Imports
- CSRFMiddleware
from starlette_csrf import CSRFMiddleware
Quickstart
import os
import uvicorn
from fastapi import FastAPI, Request, Response, Form
from starlette.middleware import Middleware
from starlette.routing import Route
from starlette.responses import HTMLResponse
from starlette_csrf import CSRFMiddleware
# Ensure you have a strong secret key
SECRET_KEY = os.environ.get('STARLETTE_CSRF_SECRET', 'a-very-secret-key-that-you-should-change-in-production')
app = FastAPI(
middleware=[
Middleware(CSRFMiddleware, secret=SECRET_KEY)
]
)
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
# The CSRF token is automatically set in a cookie on GET requests
# and can be accessed via request.state.csrftoken for templates.
# In a real application, you'd embed this in your HTML forms.
token = request.state.csrftoken if hasattr(request.state, 'csrftoken') else 'No token (GET request initial load)'
return f'''
<html>
<head>
<title>CSRF Test</title>
</head>
<body>
<h1>Welcome!</h1>
<p>CSRF Token in state (for display only): {token}</p>
<form method="post" action="/submit">
<input type="text" name="item" placeholder="Enter item">
<!-- In a real frontend, you'd get this from a cookie or initial GET response -->
<input type="hidden" name="x-csrftoken" value="{{request.state.csrftoken}}">
<button type="submit">Submit</button>
</form>
<script>
// For AJAX requests, you'd extract the csrftoken cookie and send it in the header
// Example (conceptual, requires frontend JS to read cookie):
// const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken=')).split('=')[1];
// fetch('/submit', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded',
// 'x-csrftoken': csrfToken
// },
// body: 'item=ajax_test'
// });
</script>
</body>
</html>
'''
@app.post("/submit")
async def submit_item(item: str = Form(...), response: Response = None):
# The middleware automatically validates the token from the 'x-csrftoken' header
# If validation fails, it returns a 403 Forbidden before this handler is called.
return {"message": f"Item '{item}' received successfully!"}
if __name__ == "__main__":
# To run: uvicorn your_app_file_name:app --reload
# Then open http://127.0.0.1:8000
uvicorn.run(app, host="127.0.0.1", port=8000)