Cache Poisoning in Restify
How Cache Poisoning Manifests in Restify
Cache poisoning in Restify applications occurs when an attacker manipulates HTTP caching mechanisms to serve malicious or unintended content to other users. Restify's built-in support for HTTP caching through plugins like restify-caching and its integration with HTTP headers creates specific attack vectors that developers must understand.
The most common Restify cache poisoning pattern involves manipulating the Vary header. When Restify applications dynamically generate responses based on request headers like User-Agent, Accept-Language, or custom headers, improper handling can create cache key collisions. Consider this vulnerable pattern:
const restify = require('restify');
const server = restify.createServer();
An attacker can exploit this by sending requests with crafted User-Agent headers that include cache-busting characters or by manipulating the Vary header logic. Restify's default behavior doesn't validate or sanitize vary-by headers, allowing attackers to create cache entries that collide with legitimate content.
Another Restify-specific vulnerability involves query parameter handling in cached endpoints. When using Restify's query parser plugin, unvalidated query parameters can lead to cache poisoning:
const restify = require('restify');
const server = restify.createServer();
server.use(restify.plugins.queryParser());
server.get('/search', (req, res, next) => {
// Vulnerable: No validation of query parameters
res.setHeader('Cache-Control', 'public, max-age=3600');
const results = performSearch(req.query.q);
res.json(results);
next();
});
An attacker can craft query parameters that cause the cache to store malicious responses under legitimate cache keys, which are then served to other users.
Restify-Specific Detection
Detecting cache poisoning in Restify applications requires examining both the application code and runtime behavior. middleBrick's API security scanner includes specific checks for Restify cache poisoning vulnerabilities by analyzing how your application handles caching headers and request parameters.
middleBrick scans for Restify-specific patterns including:
- Dynamic Vary header usage without proper validation
- Query parameter handling in cached endpoints
- Header manipulation vulnerabilities in Restify middleware chains
- Cache key collision possibilities based on request normalization
To manually detect cache poisoning in your Restify application, examine your middleware stack and caching configuration. Look for patterns like:
# Check for Vary header usage in your Restify routes
grep -r "Vary" routes/ || echo "No Vary headers found"
# Look for query parameter usage in cached endpoints
grep -r "Cache-Control.*max-age" routes/ | grep -E "(query|req\.query)"
middleBrick's scanning process tests these vulnerabilities by sending requests with manipulated headers and parameters, then analyzing the cache behavior and response variations. The scanner can identify if your Restify application is vulnerable to cache poisoning by attempting to:
- Manipulate Vary headers to create cache collisions
- Craft query parameters that bypass cache validation
- Test header-based content variations that could be exploited
The scanner provides specific findings with Restify context, showing exactly which routes and middleware configurations are vulnerable to cache poisoning attacks.
Restify-Specific Remediation
Securing Restify applications against cache poisoning requires implementing proper validation and normalization. Here are Restify-specific remediation techniques:
1. Validate and Normalize Vary Headers
const restify = require('restify');
const server = restify.createServer();
// Safe Vary header implementation
function setSafeVaryHeaders(res, varyHeaders) {
const allowedVaryHeaders = ['accept', 'accept-language', 'user-agent'];
const safeHeaders = varyHeaders.filter(header =>
allowedVaryHeaders.includes(header.toLowerCase())
);
if (safeHeaders.length > 0) {
res.setHeader('Vary', safeHeaders.join(', '));
}
}
server.get('/api/data', (req, res, next) => {
// Only allow safe vary headers
setSafeVaryHeaders(res, ['User-Agent']);
res.setHeader('Cache-Control', 'public, max-age=3600');
// Normalize User-Agent before processing
const normalizedUA = normalizeUserAgent(req.headers['user-agent']);
const data = generateDataBasedOnUserAgent(normalizedUA);
res.json(data);
next();
});
2. Implement Query Parameter Validation
const restify = require('restify');
const server = restify.createServer();
server.use(restify.plugins.queryParser());
// Safe query parameter handling
function validateSearchParams(params) {
const allowedParams = ['q', 'page', 'limit'];
const sanitized = {};
for (const key in params) {
if (allowedParams.includes(key)) {
sanitized[key] = sanitizeInput(params[key]);
}
}
return sanitized;
}
server.get('/search', (req, res, next) => {
const safeParams = validateSearchParams(req.query);
res.setHeader('Cache-Control', 'public, max-age=3600');
const results = performSearch(safeParams.q);
res.json(results);
next();
});
3. Use Cache Key Normalization
const restify = require('restify');
const server = restify.createServer();
// Custom cache key generator
function generateCacheKey(req) {
const normalizedPath = req.getPath().toLowerCase();
const normalizedQueryParams = Object.keys(req.query)
.sort()
.map(key => `${key}=${req.query[key]}`)
.join('&');
return `${normalizedPath}?${normalizedQueryParams}`;
}
server.get('/api/data', (req, res, next) => {
const cacheKey = generateCacheKey(req);
// Check cache with normalized key
const cachedResponse = cache.get(cacheKey);
if (cachedResponse) {
res.send(cachedResponse);
return next();
}
// Generate response and cache with normalized key
const data = generateData(req);
cache.set(cacheKey, data);
res.json(data);
next();
});
4. Implement Cache Poisoning Detection Middleware
function cachePoisoningProtection(req, res, next) {
// Detect suspicious Vary header manipulation
const varyHeader = req.headers['vary'];
if (varyHeader && /[^a-z0-9,-]/i.test(varyHeader)) {
return next(new restify.BadRequestError('Invalid Vary header'));
}
// Detect suspicious query parameters
const suspiciousParams = Object.keys(req.query).filter(param =>
/[^a-zA-Z0-9-]/.test(param)
);
if (suspiciousParams.length > 0) {
console.warn('Suspicious query parameters detected:', suspiciousParams);
// Sanitize or reject based on your security policy
}
next();
}
// Apply protection middleware
server.use(cachePoisoningProtection);