Cache Poisoning in Express with Api Keys
Cache Poisoning in Express with Api Keys — how this specific combination creates or exposes the vulnerability
Cache poisoning in an Express API that uses API keys occurs when an attacker causes cached responses to vary by attacker-controlled data, such as a key value, and those cached responses are later served to other users. This is a BFLA/Privilege Escalation and Property Authorization issue: the cache treats distinct key contexts as equivalent or insufficiently isolated, allowing one user to see another user’s data or elevated behavior. A common pattern is using the API key header to personalize data but forgetting to include it in cache key derivation, so a public or shared cache entry is reused across keys.
Consider an Express endpoint that returns user profile data and uses an API key for access control but does not incorporate the key into the cache key. If the response is cached at a CDN or in application-level caching based only on the request path, an authenticated user could request /profile with their key and receive a valid response cached under that path. A second user could then request /profile without a key or with a different key and receive the first user’s cached data, leading to data exposure. Even when authorization checks happen before caching, if the cache key does not reflect the key context, the authorization boundary is bypassed in the cache layer.
Another scenario involves query parameters that should differentiate cache entries but are omitted from the key. For example, an endpoint /reports?type=summary might return different data per API key, but if the cache key excludes the key header or a tenant identifier, responses become cross-contaminated. This intersects with BOLA/IDOR when the cached resource contains identifiers that should be scoped to a subject but are not properly segregated by key. MiddleBrick detects these cache-poisoning patterns during its 12 parallel checks, including BFLA/Privilege Escalation, Property Authorization, and Unsafe Consumption, by correlating OpenAPI/Swagger definitions with runtime behavior and ensuring that key-sensitive paths are validated for proper isolation.
In the OpenAPI spec, ensure that security schemes for API keys are declared and that paths requiring keys are not marked as non-secured or inconsistently secured. Runtime findings will highlight cases where key-bearing requests produce responses that could be cached without key inclusion. The scanner does not fix the cache configuration but provides remediation guidance, such as incorporating the API key (or a canonical representation of key-scoped data) into the cache key and enforcing strict tenant or subject scoping in cached resources.
Api Keys-Specific Remediation in Express — concrete code fixes
To mitigate cache poisoning when using API keys in Express, ensure the API key (or a normalized key scope) is part of the cache key, and avoid caching responses that contain key-specific data unless the key is guaranteed to be part of the derivation. Below are concrete, secure patterns you can apply.
Example 1: Key-aware cache key generation
Use a caching layer (e.g., Redis) where the cache key explicitly includes the API key or a tenant/subject derived from it. Never use only the route as the cache key when responses vary by key.
const crypto = require('crypto');
function cacheKeyFor(req) {
// Normalize: include a key-derived component to isolate caches per key scope
const keyId = req.get('X-API-Key') || 'anonymous';
const keyHash = crypto.createHash('sha256').update(keyId).digest('hex').slice(0, 16);
return `profile:${keyHash}`;
}
app.get('/profile', (req, res) => {
const key = cacheKeyFor(req);
redis.get(key, (err, cached) => {
if (cached) {
return res.json(JSON.parse(cached));
}
// fetch user profile bound to the key
db.getProfileByKey(keyId, (err, profile) => {
if (err) return res.status(500).json({ error: 'server' });
redis.setex(key, 300, JSON.stringify(profile)); // cache with key-aware key
res.json(profile);
});
});
});
Example 2: Disallow caching of key-authenticated responses at shared caches
If you must use a shared cache (e.g., CDN), prevent caching when an API key is present, or ensure Vary headers correctly differentiate key contexts. The following Express middleware sets appropriate cache-control headers and avoids caching sensitive responses.
app.use((req, res, next) => {
const hasKey = Boolean(req.get('X-API-Key'));
if (hasKey) {
// Do not allow shared caches to store key-specific responses
res.set('Cache-Control', 'no-store, private');
res.set('Vary', 'X-API-Key');
} else {
// Public caching allowed when no key is present
res.set('Cache-Control', 'public, max-age=300');
}
next();
});
Example 3: Validate key scope before caching in application memory
When implementing in-memory caching, ensure cached objects are tagged with the key scope and that retrieval validates the same scope. This prevents cross-key contamination in the same process.
const cache = new Map();
app.get('/data', (req, res) => {
const keyId = req.get('X-API-Key');
if (!keyId) return res.status(401).json({ error: 'missing key' });
const cacheKey = `data:${keyId}`;
const entry = cache.get(cacheKey);
if (entry) {
return res.json(entry);
}
// compute and store scoped data
const data = computeDataForKey(keyId);
cache.set(cacheKey, data);
res.json(data);
});
These patterns address BFLA/Privilege Escalation and Property Authorization by ensuring cached data cannot be retrieved across distinct API key contexts. They complement middleBrick’s checks, which include API key–specific tests for BFLA/Privilege Escalation and Unsafe Consumption; findings will point to missing key inclusion in cache derivation and suggest incorporating the key into cache keys and Vary headers.