Cache Poisoning in Koa with Api Keys
Cache Poisoning in Koa with Api Keys — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker manipulates a cache so that malicious content is served to other users. In Koa, this can arise when API keys or other request attributes are used to vary cache keys without proper validation or normalization. If a Koa application includes raw query parameters or headers such as api_key directly in the cache key, an attacker can force different responses to be cached under the same key, leading to one user receiving another user’s data or poisoned content.
Consider a scenario where a public endpoint uses a query parameter api_key for identification but does not sanitize or scope it before caching. An attacker could supply a crafted api_key that causes the application to cache a response containing sensitive information or malicious script. Because the cache key includes the api_key value, other users requesting the same path might inadvertently receive the attacker’s cached response, exposing private data or enabling stored XSS in the context of API responses.
This risk is compounded when the Koa app proxies requests to backend services and caches based on a combination of path and selected headers without stripping or hashing sensitive values such as api_key. If the api_key is treated as part of the cache key without normalization, two requests that are functionally identical but differ only in the api_key value will create separate cache entries, potentially bypassing intended isolation and enabling cross-user cache contamination.
Api Keys-Specific Remediation in Koa — concrete code fixes
To mitigate cache poisoning related to API keys in Koa, avoid using raw API key values directly in cache keys. Instead, hash or omit sensitive identifiers, and ensure cache normalization aligns with the intended scope of cached data. Below are concrete code examples for a Koa-based API using API keys safely.
Example 1: Removing api_key from cache key
Strip the api_key query parameter before constructing the cache key, ensuring that the cache is shared only where appropriate.
const Koa = require('koa');
const crypto = require('crypto');
const app = new Koa();
app.use(async (ctx, next) => {
// Remove api_key from query before caching logic
const { api_key, ...queryWithoutKey } = ctx.query;
const queryString = new URLSearchParams(queryWithoutKey).toString();
const cacheKey = crypto.createHash('sha256').update(ctx.path + '?' + queryString).digest('hex');
const cached = await getFromCache(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
// Store response in cache using normalized key
await setInCache(cacheKey, ctx.body);
});
// Dummy cache functions
async function getFromCache(key) { /* ... */ }
async function setInCache(key, value) { /* ... */ }
app.listen(3000);
Example 2: Hashing api_key to scope cache per user without exposing raw key
Use a salted hash of the API key to scope cached data to a particular consumer while preventing key leakage through cache entries.
const Koa = require('koa');
const crypto = require('crypto');
const app = new Koa();
// Salt should be stored securely, e.g., from environment
const CACHE_SALT = process.env.CACHE_SALT || 'static_salt_rotate_me';
app.use(async (ctx, next) => {
const rawKey = ctx.query.api_key || ctx.request.headers['x-api-key'] || '';
const userScope = crypto.createHmac('sha256', CACHE_SALT)
.update(rawKey)
.digest('hex');
const cacheKey = `v1:${userScope}:${ctx.path}:${JSON.stringify(ctx.query)}`;
const cached = await getFromCache(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
await setInCache(cacheKey, ctx.body);
});
// Dummy cache functions
async function getFromCache(key) { /* ... */ }
async function setInCache(key, value) { /* ... */ }
app.listen(3000);
Best practices
- Never include raw API keys in cache keys; use normalized forms or hashes.
- Validate and normalize query parameters and headers before using them to determine cache scope.
- Scope caches per consumer where necessary, but ensure that scoping does not inadvertently mix data between consumers.