Cache Poisoning in Hapi with Api Keys
Cache Poisoning in Hapi with Api Keys — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes a cache to store malicious content, leading to that content being served to other users. In Hapi, using Api Keys for access control can inadvertently expose this vulnerability when keys are handled in ways that cacheable responses vary by key but the caching layer does not differentiate them. If a server caches a response based on the request path alone while the Authorization header (or a custom header like x-api-key) changes per client, the first request that includes an authenticated user’s Api Key may be stored and later served to another user who should not see that personalized or sensitive data.
Consider a Hapi route where the response is cacheable and the server uses an Api Key passed via header for identification but does not include that header in the cache key. A route that returns user-specific data (e.g., account balances or private configuration) could be cached once with User A’s Api Key, and subsequent requests from User B might receive User A’s cached response. This breaks confidentiality and can lead to information disclosure across users. The risk is higher when responses contain sensitive data and caching is performed at a reverse proxy or CDN layer that does not inspect headers used for authorization.
Additionally, if Api Keys are reflected in URLs as query parameters (e.g., ?api_key=xxx), those URLs may be logged, shared, or cached by intermediaries, further increasing exposure. Hapi applications that rely on built-in caching or external caches must ensure that sensitive headers contributing to authorization are included in the cache key, or that authenticated responses are marked as private or not cacheable. The LLM/AI Security checks in middleBrick can detect scenarios where endpoints return sensitive data without proper cache controls, highlighting risks where Api Key usage interacts with caching behavior.
Api Keys-Specific Remediation in Hapi — concrete code fixes
To mitigate cache poisoning when using Api Keys in Hapi, ensure that responses are either not cached or cached in a way that incorporates the key used for authorization. Below are concrete, working examples that demonstrate secure handling.
1. Do not cache responses that vary by Api Key
Set cache-control headers to prevent caching of authenticated responses. This is the safest approach when responses are user-specific.
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
server.route({
method: 'GET',
path: '/account',
options: {
auth: false, // Api Key validation handled via custom logic
handler: (request, h) => {
const apiKey = request.headers['x-api-key'];
if (!apiKey || !isValidKey(apiKey)) {
return { error: 'Unauthorized' };
}
const data = getAccountDataForKey(apiKey);
// Prevent caching of sensitive, key-specific data
h.response(data)
.header('Cache-Control', 'no-store, no-cache, must-revalidate, private')
.header('Pragma', 'no-cache')
.header('Expires', '0');
return data;
},
cache: { /* intentionally omitted to avoid accidental caching */ }
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
function isValidKey(key) {
// Replace with your key validation logic
return key === 'valid-key-123';
}
function getAccountDataForKey(key) {
// Replace with actual data lookup
return { balance: 100, currency: 'USD' };
}
init();
2. Include Api Key in cache key when caching is required
If caching is necessary, vary the cache by the Api Key so that different keys produce separate cache entries.
const Hapi = require('@hapi/hapi');
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
server.route({
method: 'GET',
path: '/public-data',
options: {
handler: (request, h) => {
const apiKey = request.headers['x-api-key'];
if (!apiKey || !isValidKey(apiKey)) {
return { error: 'Unauthorized' };
}
// Simulated cache-aware response: cache varies by key
return h.response({ data: 'public-value', for: apiKey })
.header('Cache-Control', 'public, max-age=3600, vary=x-api-key')
.etag(apiKey); // ETag includes key to differentiate caches
},
cache: {
expiresIn: 60 * 60 * 1000,
privacy: 'public'
}
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
function isValidKey(key) {
return key === 'valid-key-123';
}
init();
3. Validate and sanitize Api Key usage
Ensure that Api Keys are not reflected in URLs or logs in a way that leads to cacheable URLs. Use headers only and avoid query parameters for sensitive keys.
4. Use middleware to enforce secure caching policies
Add a server extension to validate cache-related headers for routes that handle key-sensitive data.
server.ext('onPreResponse', (request, h) => {
const response = request.response;
if (response.variety === 'view' || response.variety === 'representation') {
const apiKey = request.headers['x-api-key'];
if (apiKey && isValidKey(apiKey)) {
// Ensure sensitive responses are not cached improperly
if (!response.headers['cache-control']) {
response.header('Cache-Control', 'no-store');
}
}
}
return h.continue;
});