Cache Poisoning in Koa with Bearer Tokens
Cache Poisoning in Koa with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes a cache to store a malicious response that is later served to other users. In a Koa application that uses Bearer Tokens for authorization, caching can become unsafe if responses are cached based only on the request URL and not on the presence or value of the Authorization header. If a server caches a user-specific resource (for example, a user profile or account statement) while authenticated with a Bearer Token, and that cached response is subsequently served to another user who does not have permission to see the data, this becomes a broken access control and sensitive data exposure issue.
Consider a Koa route that returns private data without including the Authorization header in cache keying. An authenticated request with a valid Bearer Token might receive personalized data, but if the caching layer treats the response as public, a subsequent unauthenticated request to the same URL could receive the cached sensitive data. This is particularly dangerous when responses include tokens, PII, or account information. The risk is compounded if the server sets long max-age values or shared caches (e.g., CDN or reverse proxy caches) do not differentiate between authenticated and unauthenticated responses.
Real-world attack patterns resemble Insecure Direct Object References (IDOR) and Broken Object Level Authorization (BOLA), where access control is bypassed via cached content. For example, an endpoint like /api/v1/users/me/transactions might return detailed transaction data for the authenticated user. If this response is cached without considering the Bearer Token, an attacker could retrieve another user’s transaction history by requesting the same URL from a victim who previously accessed it while authenticated. This maps to OWASP API Top 10 categories such as Broken Object Level Authorization (BOLA) and Sensitive Data Exposure.
In practice, you should audit whether your Koa server includes the Authorization header or a user scope in cache keys when serving personalized responses. Responses that contain authentication tokens, account numbers, or role information must never be cached in a way that allows cross-user exposure. Even if caching improves performance, omitting the Authorization header from cache differentiation effectively exposes a vector for data leakage through cache poisoning.
Bearer Tokens-Specific Remediation in Koa — concrete code fixes
To prevent cache poisoning when using Bearer Tokens in Koa, ensure that cached responses are segregated by user identity or by the presence of authorization context. One approach is to include a user-specific identifier derived from the Bearer Token in the cache key. Below is an example of a Koa middleware that extracts the user ID from a Bearer Token and incorporates it into a cache key before proceeding to the route handler.
// Example Koa middleware to build a cache key that includes user identity
const parseBearerToken = (header) => {
if (!header || !header.startsWith('Bearer ')) return null;
return header.split(' ')[1];
};
// Mock function to validate token and extract user ID (replace with real logic)
const verifyTokenAndGetUserId = async (token) => {
// In production, verify the token signature and retrieve claims
// For example, using jsonwebtoken: jwt.verify(token, publicKey)
if (token === 'valid_token_user123') return 'user-123';
if (token === 'valid_token_user456') return 'user-456';
return null;
};
app.use(async (ctx, next) => {
const authHeader = ctx.request.header.authorization;
const token = parseBearerToken(authHeader);
if (!token) {
ctx.status = 401;
ctx.body = { error: 'unauthorized' };
return;
}
const userId = await verifyTokenAndGetUserId(token);
if (!userId) {
ctx.status = 401;
ctx.body = { error: 'invalid token' };
return;
}
// Attach user identifier to context for downstream use
ctx.state.userId = userId;
await next();
});
// Example route with cache key that includes userId
app.use(async (ctx, next) => {
const userId = ctx.state.userId;
const cacheKey = `transactions:${userId}`;
// Use cacheKey with your caching backend (e.g., Redis) to store/retrieve
// This ensures responses are isolated per user
const cached = await cache.get(cacheKey);
if (cached) {
ctx.body = cached;
return;
}
await next();
// After handler, store response in cache if appropriate
// await cache.set(cacheKey, ctx.body, { ttl: ... });
});
Another remediation strategy is to avoid caching personalized responses altogether. For endpoints that return sensitive or user-specific data, explicitly set cache-control headers to prevent caching by shared caches. Below is a Koa route example that ensures private responses are not cached when Bearer Token authentication is used.
// Example Koa route that prevents caching of sensitive, user-specific data
const Router = require('koa-router');
const router = new Router();
router.get('/api/v1/users/me/profile', async (ctx) => {
const authHeader = ctx.request.header.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
ctx.status = 401;
ctx.body = { error: 'unauthorized' };
return;
}
const token = authHeader.split(' ')[1];
const userId = await verifyTokenAndGetUserId(token);
if (!userId) {
ctx.status = 401;
ctx.body = { error: 'invalid token' };
return;
}
// Fetch user-specific data
const profile = await getUserProfile(userId);
// Prevent caching by shared caches to avoid poisoning
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
ctx.set('Pragma', 'no-cache');
ctx.set('Expires', '0');
ctx.body = profile;
});
These examples demonstrate how to incorporate Bearer Token context into cache differentiation and how to enforce private caching behavior. In production, integrate these patterns with your authentication and caching layers to ensure that cached responses are never shared across users with different authorization contexts. Regularly test your endpoints to verify that cache isolation is preserved and that sensitive data is not inadvertently exposed through cached content.