Cache Poisoning in Express with Bearer Tokens
Cache Poisoning in Express with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker tricks a caching layer into storing malicious content and serving it to other users. In Express applications that use Bearer token authentication, a common misconfiguration is placing the authorization header in the cache key or failing to exclude authenticated responses from shared caches. If a cache (for example, a CDN or reverse proxy) uses the full request URI—including query parameters or headers such as Authorization—or uses a poorly normalized key, it may store a response that contains sensitive data or a token-specific payload under one user’s token and later serve it to another user.
Consider an Express route that returns user profile information and includes the Authorization header in cache decisions. A request like GET /profile?details=full with Authorization: Bearer alice_token might be cached and later served to another user who happens to hit the same cache key variant, causing token-bound data to leak across users. In addition, if the response contains sensitive data and is cached with a public cache directive or no-store directives are not enforced, an attacker who can influence query parameters or headers may cause the server to cache responses that should remain private. This is particularly risky when tokens are passed in headers and the caching logic does not explicitly exclude Authorization from cache keys, effectively turning the cache into an unintended token-bound data store.
Another scenario involves query-parameter-based cache partitioning where a token is inadvertently reflected in a URL or stored in a cached representation. For example, an endpoint that appends a session or token value to a query string for tracking could result in a cache entry that is unique per token but not isolated per user, enabling token replay or exposure through cache side-channels. Because the cache may be shared among multiple clients, a poisoned entry can lead to privilege escalation or sensitive information disclosure when the cached response is served to unauthorized users.
To identify this class of issue, scanners perform black-box testing against the running endpoint, checking whether authenticated responses are cached improperly and whether cache keys inadvertently incorporate authorization headers or tokens. Findings typically highlight missing Cache-Control: no-store on sensitive responses, overly broad Vary headers, or cache keys that include Authorization without explicit exclusion. Properly configured caches must differentiate responses per user and must never store or serve token-bound data to other principals.
Bearer Tokens-Specific Remediation in Express — concrete code fixes
Remediation focuses on ensuring that responses containing bearer token data are never cached in shared caches and that cache keys do not incorporate authorization information. In Express, you can set headers to prevent caching of sensitive responses and normalize cache keys to exclude the Authorization header.
Example: Setting no-store for authenticated responses
const express = require('express');
const app = express();
app.get('/profile', (req, res) => {
const authHeader = req.headers['authorization'];
// If a bearer token is present, treat the response as private
if (authHeader && authHeader.startsWith('Bearer ')) {
res.setHeader('Cache-Control', 'no-store, no-transform');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
}
// Normal response handling
res.json({ profile: { name: 'Alice', scope: 'read' } });
});
module.exports = app;
Example: Normalizing cache key in a caching proxy or middleware
If you use a reverse proxy or a custom cache key function, ensure the Authorization header is excluded. The following snippet demonstrates how to normalize a request URL for caching by omitting sensitive headers before generating a cache key.
function getCacheKey(req) {
// Exclude Authorization from cache key
const { authorization, ...headers } = req.headers;
const sortedQuery = new URLSearchParams(
Object.keys(req.query)
.sort()
.reduce((acc, key) => { acc[key] = req.query[key]; return acc; }, {})
).toString();
return `${req.method}:${req.path}:${sortedQuery}:${JSON.stringify(headers)}`;
}
// Example usage in middleware (conceptual)
app.use((req, res, next) => {
req.cacheKey = getCacheKey(req);
next();
});
Example: Express route with strict no-cache and Vary
app.get('/account', (req, res) => {
res.set({
'Cache-Control': 'no-store, max-age=0',
'Vary': 'Accept-Encoding', // Avoid including Authorization in Vary
'Content-Type': 'application/json'
});
res.json({ account: { number: '****3321', role: 'user' } });
});
Additional recommendations
- Do not include Authorization or Bearer tokens in URLs or cache keys; use POST or other mechanisms for sensitive operations if caching is not required.
- Explicitly set
Varyto avoid cache fragmentation on headers that should not affect the response for different users. - Ensure that responses with sensitive payloads include
Cache-Control: no-storeand related headers to prevent storage by shared caches, CDNs, or browsers.
Frequently Asked Questions
How can I verify that my Express routes do not leak token-bound data through caching?
Cache-Control: no-store and that cache keys exclude the Authorization header. Inspect caching headers and ensure shared caches do not vary on token-bearing requests.