Cache Poisoning in Fastapi with Hmac Signatures
Cache Poisoning in Fastapi with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Cache poisoning in a FastAPI service that uses HMAC signatures can occur when an attacker is able to influence cached responses through mismatched or weakly validated inputs. HMAC signatures are typically used to ensure integrity and authenticity of request or cache-key data, but if the application does not include all relevant request dimensions in the signed or cached context, an attacker can cause the same signed response to be reused for different victims or contexts.
Consider a FastAPI endpoint that caches personalized data (e.g., user settings or profile) keyed by a combination of path parameters and query parameters, and signs the cache key with an HMAC derived from a shared secret. If the implementation builds the cache key from only a subset of these values — for example, including the path parameter but omitting a user-specific header or cookie that the HMAC logic does not cover — two different users may produce the same cache key. An attacker who can observe or guess another user’s identifier might craft a request that reuses a cached, signed response intended for someone else, leading to sensitive data exposure or unauthorized content delivery.
Another scenario involves query parameters that affect the response but are excluded from the cache key or HMAC input. For instance, a FastAPI endpoint might return different representations based on an Accept-Language or a tenant identifier query parameter. If the cache key ignores these parameters but the HMAC is computed only over the path and a static subset, an attacker can manipulate the unhashed parameter to receive a cached response meant for a different language or tenant. Because the signature validates only part of the request context, the server may incorrectly treat the poisoned cache entry as valid and authoritative.
In a FastAPI application, this often stems from inconsistent handling between cache key construction and HMAC verification. For example, if the signature is computed over a serialized payload or specific headers but the cache key is derived from a different subset of request attributes, an attacker can exploit the mismatch. Common weaknesses include failure to include the full set of differentiating inputs in both the cache key and the HMAC, or failure to bind the HMAC to the caching layer’s key space. Such omissions effectively reduce the integrity protection to a partial check, enabling an authenticated or unauthenticated attacker to leverage cached responses in unintended ways, which may violate confidentiality or integrity expectations for the API.
Hmac Signatures-Specific Remediation in Fastapi — concrete code fixes
To remediate cache poisoning when using HMAC signatures in FastAPI, ensure that the cache key and the HMAC input cover the same set of request dimensions that materially affect the response. This includes path parameters, query parameters that change the representation, selected headers (e.g., authentication or tenant identifiers), and any other context that should be integrity-protected. Below are concrete code examples demonstrating a robust approach.
First, define a utility to canonicalize and sign the relevant request components. This example uses SHA256 for the HMAC and includes the HTTP method, path, selected query parameters, and a tenant header:
import hashlib
import hmac
import urllib.parse
from typing import List
def build_canonical_string(
method: str,
path: str,
query_params: dict,
headers: dict,
keys_to_include: List[str],
secret: bytes,
) -> bytes:
# Deterministically serialize selected query parameters
parts = []
for k in keys_to_include:
if k in query_params:
val = query_params[k]
if isinstance(val, list):
for v in val:
parts.append(f"{k}={v}")
else:
parts.append(f"{k}={val}")
else:
parts.append(f"{k}=")
# Include a stable header subset
header_part = ";".join(f"{k}={headers.get(k, '')}" for k in ["X-Tenant-ID", "Accept"])
canonical = f"{method}|{path}|{'|'.join(parts)}|{header_part}"
return hmac.new(secret, canonical.encode("utf-8"), hashlib.sha256).digest()
In your FastAPI route, compute the signature and use a composite cache key that mirrors the canonical string scope:
from fastapi import FastAPI, Request, HTTPException
import hashlib
app = FastAPI()
SECRET = b"your-secure-secret"
KEYS = ["version", "locale", "tenant"]
def cache_key_for_request(request: Request) -> str:
query = dict(request.query_params)
headers = {"X-Tenant-ID": request.headers.get("X-Tenant-ID", ""), "Accept": request.headers.get("Accept", "")}
sig = build_canonical_string(
method=request.method,
path=request.url.path,
query_params=query,
headers=headers,
keys_to_include=KEYS,
secret=SECRET,
)
digest = hashlib.sha256(sig).hexdigest()
return f"v1:{request.method}:{request.url.path}:{digest}"
@app.get("/items/{item_id}")
async def read_item(
request: Request,
item_id: int,
version: str,
locale: str,
tenant: str,
):
key = cache_key_for_request(request)
# pseudo-cache lookup/store — replace with your actual cache backend
cached = CACHE_GET(key)
if cached is not None:
return cached
# compute response
result = {"item_id": item_id, "version": version, "locale": locale, "tenant": tenant}
CACHE_SET(key, result, ttl=60)
return result
This approach binds the cache key to the same inputs used in the HMAC, preventing an attacker from swapping cached responses across differing query or header values. Additionally, always include all mutable dimensions that affect the response in both the canonical string and the cache key, and avoid omitting headers or parameters that change authorization or representation. Rotate the shared secret periodically and enforce strict allowed values for tenant or locale inputs to reduce the impact of any potential cache poisoning attempt.