Cache Poisoning in Fastapi with Basic Auth
Cache Poisoning in Fastapi with Basic Auth — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker tricks a caching layer into storing malicious content that is subsequently served to other users. In Fastapi applications that use HTTP Basic Authentication for some routes but rely on a shared cache (for example, an upstream reverse proxy or CDN), this risk is amplified when cached responses are shared across users with different credentials.
Consider a Fastapi endpoint that returns user-specific data and uses Basic Auth for access control but also sets cache headers that encourage upstream caching. If the application does not include the Authorization header as a cache key (or includes it improperly), the cache may store a response for one authenticated user and serve it to another user. This can lead to information disclosure where one user sees another user’s data, or to cache injection where an attacker’s crafted response is cached and served broadly.
An example scenario: a GET endpoint /api/user/profile requires Basic Auth, and the Fastapi app sets Cache-Control: public, max-age=60. If the caching layer ignores the Authorization header, two different users’ requests may map to the same cache key. User A’s authenticated response can be stored and later served to User B, leaking User A’s profile data. Additionally, an authenticated attacker could inject a malicious response (e.g., by including crafted query parameters or body content that is improperly reflected) and cause the cache to store that payload, leading to widespread exposure.
With Basic Auth, the credentials are sent in the request headers on every call. If the caching infrastructure does not treat the Authorization header as part of the cache key—and ideally does not cache responses for requests containing Authorization at all—this creates a clear path for cache poisoning. The problem is not Basic Auth itself, but the interaction between per-user credentials and shared caching that ignores or misuses those credentials.
To detect such issues, scans should check whether responses with Authorization headers are being cached and whether Vary headers are used correctly. Missing or incorrect Vary: Authorization is a common sign that user-specific content may be mishandled by caches. Without proper Vary rules, intermediaries may serve a cached response to users for whom it is not intended, effectively bypassing authentication boundaries at the cache layer.
Basic Auth-Specific Remediation in Fastapi — concrete code fixes
Remediation focuses on ensuring user-specific responses are not shared across users and that caching behavior is explicit and safe. In Fastapi, you should avoid caching responses that include Authorization headers unless you can guarantee isolation, and you must use Vary headers correctly when caching user-specific content.
Example of unsafe behavior to avoid:
from fastapi import Fastapi, Depends, Header
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = Fastapi()
security = HTTPBasic()
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
# Validate credentials and return user
return {"username": credentials.username}
@app.get("/api/user/profile")
async def profile(user: dict = Depends(get_current_user)):
# This response should NOT be cached publicly if it is user-specific
return {"profile": "sensitive_data_for_" + user["username"]}
Safer approach: prevent caching of authenticated responses or isolate cache by user. Do not set public cache directives on user-specific endpoints. If you must cache, use Vary: Authorization and ensure the cache key includes credentials or user identifiers, or use private caching only.
Corrected example with explicit no-store for sensitive routes:
from fastapi import Fastapi, Depends, Header, Response
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = Fastapi()
security = HTTPBasic()
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
return {"username": credentials.username}
@app.get("/api/user/profile")
async def profile(user: dict = Depends(get_current_user), response: Response = None):
# Prevent caching of sensitive, user-specific responses
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private"
response.headers["Vary"] = "Authorization"
return {"profile": "safe_for_" + user["username"]}
If you need to cache at the edge for authenticated scenarios, use private caching and ensure Authorization is part of the cache key. Do not rely on default behavior from intermediaries. Also consider moving sensitive endpoints behind paths that are excluded from caching, or require revalidation on each request.
Additionally, enforce strict Transport Layer Security and ensure credentials are never logged or reflected. Combine these measures with runtime scans to verify that responses with Authorization are not served with permissive caching rules.