Cache Poisoning in Koa
How Cache Poisoning Manifests in Koa
Cache poisoning in Koa applications typically occurs when user-controlled input influences cache keys or cache storage without proper validation. The most common scenario involves query parameters that affect response content but aren't properly isolated in the cache layer.
Consider a Koa route that serves different content based on a 'theme' parameter:
app.use(async (ctx) => {
const theme = ctx.query.theme || 'default';
ctx.body = await renderTemplate(theme);
});If this response is cached without considering the theme parameter, one user's theme selection could be served to others. The cache key generation becomes critical here—Koa's default behavior doesn't automatically include query parameters in cache keys.
Another Koa-specific manifestation occurs with middleware ordering. If a caching middleware executes before authentication or parameter validation, poisoned data can be stored before security checks complete:
// Problematic ordering
app.use(cacheMiddleware);
app.use(authMiddleware);
app.use(routeHandler);Cross-site cache poisoning can also occur when Koa applications serve different content types based on Accept headers or other request metadata. An attacker could craft requests that cause the cache to store malicious content under legitimate cache keys.
Koa's flexible middleware system means cache poisoning vectors can appear in unexpected places—custom error handlers, logging middleware, or even response transformers can inadvertently introduce cache poisoning vulnerabilities if they modify responses based on user input.
Koa-Specific Detection
Detecting cache poisoning in Koa requires examining both application code and runtime behavior. Start by auditing middleware ordering and cache key generation logic.
Code review should focus on these patterns:
// Check for missing query parameter handling
app.use(async (ctx) => {
const data = await getData(ctx.query.id);
ctx.body = data;
// Missing: cache key includes query parameters?
});middleBrick's black-box scanning can identify cache poisoning vulnerabilities by testing how different inputs affect cached responses. The scanner sends multiple requests with varying parameters and checks if responses are incorrectly shared between different user contexts.
Runtime detection involves monitoring cache hit rates and response variations. Tools like koa-conditional-get and koa-etag can help identify when caching behavior doesn't align with content variations.
middleBrick specifically tests for:
- Query parameter manipulation affecting cached content
- Header-based content variation without proper cache isolation
- Authentication state changes not reflected in cache keys
- Content-Type variations leading to cache poisoning
The scanner's LLM security module also checks for AI-specific cache poisoning where model responses might be cached and served to unauthorized users.
Koa-Specific Remediation
Remediating cache poisoning in Koa requires proper cache key generation and middleware ordering. The most effective approach uses a cache key generator that includes all relevant request parameters:
const createCacheKey = (ctx) => {
return `${ctx.path}:${JSON.stringify(ctx.query)}:${ctx.accepts()}`;
};
app.use(async (ctx, next) => {
const cacheKey = createCacheKey(ctx);
const cached = await cache.get(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
await cache.set(cacheKey, ctx.body);
});For applications using koa-router, parameter-based cache keys are essential:
const router = new Router();
router.get('/users/:id', async (ctx) => {
const userId = ctx.params.id;
const cacheKey = `user:${userId}`;
const cached = await cache.get(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
const user = await getUser(userId);
ctx.body = user;
await cache.set(cacheKey, user);
});Authentication-aware caching prevents privilege escalation through cached responses:
const authAwareCache = async (ctx, next) => {
if (!ctx.state.user) {
// Public content, cache normally
await next();
} else {
// Private content, include user ID in cache key
const cacheKey = `${ctx.path}:${ctx.state.user.id}`;
const cached = await cache.get(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
await cache.set(cacheKey, ctx.body);
}
};
Content variation handling requires careful cache control headers:
app.use(async (ctx, next) => {
await next();
// Set appropriate cache headers based on content type
if (ctx.type === 'application/json') {
ctx.set('Cache-Control', 'public, max-age=300');
} else if (ctx.type.startsWith('text/')) {
ctx.set('Cache-Control', 'public, max-age=300, must-revalidate');
} else {
ctx.set('Cache-Control', 'no-cache');
}
});Using middleBrick's CLI tool helps verify your remediation:
npx middlebrick scan https://your-koa-app.com/api/users/123?theme=dark
The tool will test if different parameter combinations produce isolated cache entries and identify any remaining poisoning vectors.