Cache Poisoning in Adonisjs
How Cache Poisoning Manifests in Adonisjs
Cache poisoning in Adonisjs applications typically occurs through the framework's built-in caching mechanisms, particularly when user-controlled input influences cache keys or when cached responses contain unvalidated data. The most common attack vectors involve the @Adonisjs/Cache module where malicious actors manipulate cache keys to overwrite legitimate entries or inject harmful content.
A typical Adonisjs cache poisoning scenario occurs in route handlers that cache user-specific data without proper isolation. Consider this vulnerable pattern:
const Cache = use('Cache')
Route.get('/user/profile', async ({ request }) => {
const userId = request.input('id', 'default')
const cacheKey = `user:${userId}:profile`
let profile = await Cache.get(cacheKey)
if (!profile) {
profile = await fetchUserProfile(userId)
await Cache.put(cacheKey, profile, 3600)
}
return profile
})The vulnerability here is that an attacker can control the id parameter and potentially overwrite cache entries for other users. If the cache backend doesn't properly isolate namespaces, one user could poison the cache for another by crafting specific IDs.
Another Adonisjs-specific manifestation occurs with template caching. When using the Edge templating engine, if user input influences template rendering without proper escaping, cached templates can serve malicious content to other users:
Route.get('/search', async ({ request }) => {
const query = request.input('q', '')
const view = await View.render('searchResults', { query })
// Caching the rendered template - dangerous if query contains malicious content
await Cache.put(`search:${query}`, view, 1800)
return view
})Cross-site scripting through cached responses is particularly dangerous in Adonisjs applications that use the Cache module with Redis or memory backends. The framework's default configuration doesn't include cache poisoning protections, making it the developer's responsibility to implement proper validation and isolation.
Adonisjs's middleware system can also introduce cache poisoning risks. A global caching middleware that doesn't account for user roles or permissions might cache privileged content and serve it to unauthorized users:
class CacheResponseMiddleware {
async handle({ request, response }, next) {
const key = request.url()
let cached = await Cache.get(key)
if (cached) {
return cached
}
await next()
if (response.status === 200) {
await Cache.put(key, response.body, 600)
}
}
}This middleware would cache responses regardless of user authentication state, potentially serving cached admin content to regular users.
Adonisjs-Specific Detection
Detecting cache poisoning in Adonisjs applications requires examining both the application code and runtime behavior. Start by auditing your Cache module usage patterns across all route handlers and middleware.
Static analysis should focus on identifying these red flags in your Adonisjs codebase:
// Patterns that indicate potential cache poisoning vulnerabilities
const dangerousPatterns = [
// User input directly in cache keys
/Cache\.get\(['"`].*request\./,
// No validation before caching
/Cache\.put\(.*request\./,
// Caching without user isolation
/Cache\.(get|put)\(.*\).*[Aa]dmin|[Ss]uper|[Pp]rivileged/
]Runtime detection involves monitoring cache operations for anomalous patterns. Implement logging in your Adonisjs application to track cache key creation and access patterns:
const originalPut = Cache.put.bind(Cache)
Cache.put = async function(key, value, ttl) {
console.log(`Cache put: ${key} (${typeof value})`)
return originalPut(key, value, ttl)
}For automated detection, middleBrick's API security scanner can identify cache poisoning vulnerabilities in Adonisjs applications by analyzing the runtime behavior of cache operations. The scanner tests for:
- Cache key manipulation through parameter tampering
- Response caching without proper content validation
- Cross-user cache contamination
- Template caching vulnerabilities
middleBrick's scanning process for Adonisjs applications includes sending crafted requests with special characters and varying parameters to observe how the cache handles different inputs. The scanner specifically looks for:
// What middleBrick tests for in Adonisjs cache endpoints
const testPatterns = [
{ param: 'id', value: '../../../etc/passwd' },
{ param: 'user', value: 'admin' },
{ param: 'query', value: '<script>alert(1)</script>' },
{ param: 'cacheKey', value: 'existingCacheKey' }
]The scanner's output includes a security score and specific findings about cache poisoning risks, with severity levels based on the potential impact of successful exploitation.
Adonisjs-Specific Remediation
Remediating cache poisoning in Adonisjs applications requires a defense-in-depth approach using the framework's built-in features and proper security practices.
First, implement strict cache key validation using Adonisjs's validator:
const { validate } = use('Validator')
async function safeCacheKey(input, pattern = /^[a-zA-Z0-9_-]+$/) {
const validation = await validate({ key: input }, {
key: 'regex:^[a-zA-Z0-9_-]+$
})
if (validation.fails()) {
throw new Error('Invalid cache key format')
}
return `app:${input}` // Namespaced keys
}
// Usage in route handlers
Route.get('/user/profile', async ({ request }) => {
const userId = request.input('id', 'default')
const cacheKey = await safeCacheKey(userId)
let profile = await Cache.get(cacheKey)
if (!profile) {
profile = await fetchUserProfile(userId)
await Cache.put(cacheKey, profile, 3600)
}
return profile
})For template caching, use Edge's built-in escaping and avoid caching raw user input:
Route.get('/search', async ({ request, view }) => {
const query = request.input('q', '')
// Validate and sanitize input
const sanitizedQuery = sanitizeInput(query)
// Render with proper escaping
const template = await view.render('searchResults', { query: sanitizedQuery })
// Cache only after validation
await Cache.put(`search:${md5(sanitizedQuery)}`, template, 1800)
return template
})
function sanitizeInput(input) {
return input
.replace(/[&<>"'/]/g, (c) => {
switch (c) {
case '&': return '&'
case '<': return '<'
case '>': return '>'
case '"': return '"'
case "'": return '''
case '/': return '/'
default: return c
}
})
.substring(0, 255) // Limit length
}Implement cache isolation using Adonisjs's middleware system:
class SecureCacheMiddleware {
async handle({ request, response, auth }, next) {
const originalPut = response.put.bind(response)
response.put = async function(body) {
if (response.status === 200) {
const key = request.url()
const userId = auth.user?.id || 'anonymous'
// Namespace cache by user and include validation
const safeKey = await safeCacheKey(`${userId}:${key}`)
// Only cache safe content types
if (typeof body === 'object' && !Buffer.isBuffer(body)) {
await Cache.put(safeKey, body, 600)
}
}
return originalPut(body)
}
return next()
}
}For Redis caching (common in production Adonisjs apps), configure proper namespace isolation:
// config/cache.js
module.exports = {
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
namespace: process.env.APP_NAME || 'adonis',
keyPrefix: (key) => {
// Add application and environment prefix
return `${process.env.NODE_ENV}:${key}`
}
}
}Finally, implement cache poisoning detection as a health check in your Adonisjs application:
Route.get('/health/cache', async () => {
try {
const testKey = 'health:cache:test'
await Cache.put(testKey, 'test-value', 1)
const retrieved = await Cache.get(testKey)
await Cache.delete(testKey)
if (retrieved !== 'test-value') {
throw new Error('Cache integrity compromised')
}
return { healthy: true }
} catch (error) {
return { healthy: false, error: error.message }
}
})