HIGH cache poisoningfastapibasic auth

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.

Frequently Asked Questions

Why is Vary: Authorization important for cache safety with Basic Auth?
Vary: Authorization tells intermediaries to use the Authorization header as part of the cache key. Without it, a cached response from one authenticated user might be served to another, causing cache poisoning and information disclosure.
Can I safely cache authenticated endpoints with Basic Auth if I use HTTPS only?
HTTPS alone does not protect against cache poisoning. You must also ensure caching directives and Vary rules prevent user-specific responses from being shared. Prefer no-store for sensitive endpoints or use private caching with credentials included in the cache key.