HIGH cache poisoningexpress

Cache Poisoning in Express

How Cache Poisoning Manifests in Express

Cache poisoning in Express applications occurs when an attacker manipulates cache keys or injects malicious content that gets stored and served to other users. This vulnerability is particularly dangerous in Express because of its flexible middleware system and common caching patterns.

The most common Express-specific manifestation involves improper handling of Vary headers. Consider this vulnerable pattern:

app.get('/api/user', (req, res) => {
  const userId = req.query.userId || req.user.id;
  const user = getUserById(userId);
  
  res.setHeader('Cache-Control', 'public, max-age=300');
  res.json(user);
});

This endpoint is vulnerable because it doesn't properly vary the cache by user identity. An attacker can request /api/user?userId=1 then /api/user?userId=2, potentially causing the cache to serve user 1's data to user 2.

Another Express-specific pattern involves middleware ordering. When caching middleware is placed before authentication:

const cache = require('apicache');
app.use(cache.middleware('5 minutes'));
app.use(authenticate);
app.get('/protected', (req, res) => {
  res.json({ data: sensitiveInfo });
});

Even though authentication runs, the cache stores responses before authentication completes, allowing unauthenticated users to access cached protected data.

Query parameter manipulation is another attack vector. Express applications often use query parameters for filtering or pagination:

app.get('/search', (req, res) => {
  const { query, page = 1 } = req.query;
  const results = searchDatabase(query, page);
  
  res.setHeader('Cache-Control', 'public, max-age=300');
  res.json(results);
});

An attacker can craft specific query combinations that cause the cache to store poisoned responses, which are then served to legitimate users searching for similar terms.

Header-based cache poisoning is also Express-specific. Applications often use custom headers for API versioning or feature flags:

app.get('/api/v1/data', (req, res) => {
  const version = req.get('X-API-Version') || '1.0';
  const data = getDataForVersion(version);
  
  res.setHeader('Cache-Control', 'public, max-age=600');
  res.json(data);
});

Without proper Vary header configuration, different API versions can overwrite each other in the cache, potentially serving newer data to older clients or vice versa.

Express-Specific Detection

Detecting cache poisoning in Express requires examining both code patterns and runtime behavior. Start by auditing your middleware stack and caching configuration.

Code analysis should focus on these Express-specific patterns:

# Check for caching middleware placement
grep -r "cache" routes/ --include="*.js"
grep -r "Cache-Control" routes/ --include="*.js"

Look for caching middleware like apicache, express-redis-cache, or node-cache and verify they're placed after authentication and authorization middleware.

Runtime detection involves monitoring cache keys and hit rates. Use Express middleware to log cache operations:

const cache = require('apicache');

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    if (res.get('X-Cache')) {
      console.log(`Cache ${res.get('X-Cache')} for ${req.path}`);
    }
  });
  next();
});

middleBrick's Express-specific scanning can automatically detect these patterns by analyzing your running application. It tests for cache poisoning by:

  • Checking Vary header completeness for user-specific endpoints
  • Testing cache key collisions across different user contexts
  • Verifying caching middleware ordering relative to auth middleware
  • Analyzing query parameter handling for cache poisoning vectors

The scanner creates test requests with manipulated parameters and headers to observe cache behavior, identifying where user data might be improperly shared.

Express-Specific Remediation

Remediating cache poisoning in Express requires both architectural changes and proper header configuration. The most critical fix is ensuring proper Vary header usage:

app.get('/api/user', (req, res) => {
  const userId = req.user.id;
  const user = getUserById(userId);
  
  res.setHeader('Cache-Control', 'public, max-age=300');
  res.setHeader('Vary', 'Cookie, Authorization, User-ID');
  res.json(user);
});

This ensures the cache key includes user identity information, preventing cross-user data leakage.

Middleware ordering is equally important. Always place authentication before caching:

app.use(authenticate);
app.use(authorize);
app.use(cache.middleware('5 minutes'));

app.get('/protected', (req, res) => {
  res.json({ data: sensitiveInfo });
});

For query parameter endpoints, implement strict cache key generation:

const generateCacheKey = (req) => {
  const { query, params, user } = req;
  return `${req.path}:${JSON.stringify({
    query: query,
    params: params,
    userId: user?.id
  })}`;
};

app.get('/search', (req, res) => {
  const key = generateCacheKey(req);
  
  // Check cache with generated key
  // Process request and store with proper key
});

Consider using cache-busting techniques for sensitive endpoints:

app.get('/api/sensitive', (req, res) => {
  res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');
  
  res.json({ sensitive: data });
});

For applications using Redis or other external caches, implement namespace isolation:

const cache = require('apicache');
const namespace = process.env.NODE_ENV || 'development';

const namespacedCache = {
  get: (key) => cache.get(`${namespace}:${key}`),
  set: (key, value, ttl) => cache.set(`${namespace}:${key}`, value, ttl)
};

Finally, implement cache validation to prevent stale data serving:

app.use((req, res, next) => {
  const cached = res.get('X-Cache');
  if (cached && cached !== 'MISS') {
    const lastModified = req.get('If-Modified-Since');
    if (lastModified) {
      const cacheTime = new Date(cached.split(' ')[1]);
      if (cacheTime.getTime() > new Date(lastModified).getTime()) {
        res.setHeader('Cache-Control', 'must-revalidate');
      }
    }
  }
  next();
});

Frequently Asked Questions

How can I test if my Express application is vulnerable to cache poisoning?
Use middleBrick's automated scanning to test your Express endpoints. It specifically checks for improper Vary headers, caching middleware ordering, and query parameter handling that could lead to cache poisoning. You can also manually test by requesting the same endpoint with different user contexts and checking if cached responses are served across users.
What's the difference between cache poisoning and cache leakage in Express?
Cache poisoning involves injecting malicious content that gets stored and served, while cache leakage is about serving cached data to the wrong user (often due to missing Vary headers). In Express, both often manifest together - an attacker poisons the cache with manipulated data, then other users receive that poisoned data due to improper cache key generation.