Cross Site Request Forgery in Fastapi with Api Keys
Cross Site Request Forgery in Fastapi with Api Keys — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) is a web security vulnerability that forces authenticated users to execute unwanted actions on a web application in which they are authenticated. In FastAPI, using only API keys for authentication does not protect against CSRF when the API is accessed from browsers via JavaScript. API keys are often stored in cookies or local storage and sent automatically by the browser with requests to the origin, making state-changing endpoints (POST, PUT, DELETE) susceptible to CSRF if no anti-CSRF mechanisms are in place.
Consider a FastAPI endpoint that transfers funds and relies on an API key passed via a cookie:
from fastapi import FastAPI, Request, Cookie
app = FastAPI()
@app.post("/transfer")
def transfer(amount: int, to: str, api_key: str = Cookie(None)):
if not api_key:
raise HTTPException(status_code=401, detail="Missing API key")
# Process transfer — vulnerable to CSRF if the browser sends the cookie automatically
return {"status": "ok"}
If a user is logged in to your site and visits a malicious site, that site can embed a form that posts to /transfer with forged parameters. Because the browser includes the API key cookie, the request appears valid. API keys sent as cookies are automatically included by the browser in cross-origin requests, which enables CSRF. In contrast, API keys sent in the Authorization header (e.g., Bearer or a custom header) are not automatically added by browsers and are therefore less vulnerable, but they can still be at risk if the CORS configuration is permissive or if the endpoint is designed to accept credentials without proper CSRF protections.
Another angle is when the OpenAPI spec (2.0/3.0/3.1) describes cookie-based API keys without explicit security schemes that discourage browser usage, and the runtime endpoints accept credentials without CSRF mitigations. middleBrick’s OpenAPI/Swagger analysis can detect whether cookie-based API key definitions exist and whether the exposed endpoints lack anti-CSRF guidance, flagging the potential for request forgery in unauthenticated scans.
CSRF in this context is not about breaking authentication but about tricking a trusted authenticated user into performing actions they did not intend. Mitigations include anti-CSRF tokens, SameSite cookie attributes, and requiring custom headers that cannot be set cross-origin by malicious sites.
Api Keys-Specific Remediation in Fastapi — concrete code fixes
To protect FastAPI endpoints while continuing to use API keys, apply defenses that prevent browsers from automatically sending credentials and enforce explicit origin checks.
1. Use the Authorization header instead of cookies
Browsers do not automatically add custom headers like Authorization in cross-origin requests triggered by malicious sites. Require API keys via the Authorization header:
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
@app.post("/transfer")
def transfer(amount: int, to: str, authorization: str = Header(None)):
if authorization != "Bearer expected_api_key_here":
raise HTTPException(status_code=401, detail="Invalid API key")
return {"status": "ok"}
2. Enforce SameSite and Secure cookie attributes if cookies are required
If you must use cookies, configure SameSite=Lax or Strict and Secure to prevent cross-origin sending:
from fastapi import FastAPI, Response, Cookie
app = FastAPI()
@app.post("/set-api-key")
def set_api_key(response: Response, api_key: str):
response.set_cookie(
key="api_key",
value=api_key,
httponly=True,
secure=True,
samesite="strict",
)
return {"status": "cookie set"}
3. Add CSRF tokens for cookie-based flows
When cookie-based authentication is unavoidable, implement anti-CSRF tokens and validate them on state-changing operations:
from fastapi import FastAPI, Request, Cookie, Depends, HTTPException
from fastapi.responses import JSONResponse
import secrets
app = FastAPI()
_csrf_tokens = {}
@app.post("/csrf-token")
def get_csrf_token(response: Response, api_key: str = Cookie(None)):
if not api_key:
raise HTTPException(status_code=401, detail="Missing API key")
token = secrets.token_urlsafe(32)
_csrf_tokens[api_key] = token
response.set_cookie(key="csrf_token", value=token, httponly=False, secure=True, samesite="strict")
return {"csrf_token": token}
@app.post("/transfer")
def transfer(
amount: int,
to: str,
api_key: str = Cookie(None),
csrf_token: str = Cookie(None),
):
if api_key not in _csrf_tokens or _csrf_tokens[api_key] != csrf_token:
raise HTTPException(status_code=403, detail="Invalid CSRF token")
return {"status": "ok"}
4. Validate Origin and Referer headers cautiously
While not a standalone fix, checking Origin or Referer can add a layer of defense. Do not rely on it alone due to potential header omission in cross-origin requests:
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/transfer")
def transfer(
amount: int,
to: str,
request: Request,
authorization: str = Header(None),
):
if authorization != "Bearer expected_api_key_here":
raise HTTPException(status_code=401, detail="Invalid API key")
origin = request.headers.get("origin")
if origin not in (None, "https://trusted.example.com"):
raise HTTPException(status_code=403, detail="Invalid origin")
return {"status": "ok"}
5. Leverage CORS configuration to restrict origins
Use FastAPI’s CORSMiddleware to limit which origins can make requests:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://trusted.example.com"],
allow_credentials=False,
allow_methods=["POST"],
allow_headers=["Authorization"],
)
These steps reduce the risk that an API key stored or sent by browsers can be exploited via CSRF while preserving the usability of your API.