Cache Poisoning in Adonisjs with Mutual Tls
Cache Poisoning in Adonisjs with Mutual Tls
Cache poisoning occurs when an attacker causes a cache to store malicious content, leading to other users receiving that content. In AdonisJS applications that terminate mutual TLS (mTLS) at the edge or gateway layer, the interplay between client certificate validation, request normalization, and caching behavior can inadvertently enable cache poisoning.
With mTLS, the server validates the client certificate before application logic runs. AdonisJS typically sees the authenticated client identity via request attributes set by the TLS layer (for example, the certificate’s subject or serial number). If the application uses parts of the request that are not validated by mTLS—such as query parameters, headers, or the request path—to construct cache keys, an unauthenticated attacker may be able to inject values that change the cache key without failing mTLS. A common scenario is an endpoint that varies response by a query parameter like locale or tenant but only checks presence of a client certificate, not its value. The cache may key on the raw query string, causing different poisoned entries for different attackers while requests still present a valid client cert.
Another vector is header-based cache keys where an attacker injects a header that is reflected in the response yet not covered by mTLS assertions. For example, an endpoint that uses X-Forwarded-For or a custom X-Cache-Key to vary responses may allow an attacker to control the key by setting that header. Because mTLS only ensures the client possesses a trusted certificate, it does not sanitize or validate these inputs, leading to poisoned cache entries that are served to other users with valid certificates.
Additionally, if the application caches based on the certificate’s subject distinguished name (DN) but the DN is taken directly from headers or query parameters that mTLS does not validate, an attacker can supply arbitrary DNs to create distinct cache entries. This can expose user-specific data across certificate holders or cause incorrect content to be served. Proper normalization and strict validation of mTLS-derived attributes before constructing cache keys are essential to prevent cache poisoning in AdonisJS with mutual TLS.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring that cache keys incorporate only validated, canonical mTLS attributes and that untrusted inputs are excluded from cache key construction. Below are concrete, realistic code examples for an AdonisJS application using mTLS via a reverse proxy that sets trusted headers.
1. Canonicalize certificate-derived values before caching
Extract the certificate subject from the trusted header, normalize it, and use it as part of a deterministic cache key. Do not rely on raw headers or query parameters that can be influenced by the client.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import crypto from 'crypto'
export default class SessionsController {
public async getUserProfile({ request, response }: HttpContextContract) {
// Assume the reverse proxy validated mTLS and sets these headers
const rawCertDn = request.header('x-client-cert-dn')
if (!rawCertDn) {
response.status(400).send({ error: 'Client certificate required' })
return
}
// Normalize: trim, lowercase, remove whitespace variations to ensure consistent key
const normalizedDn = rawCertDn.trim().toLowerCase().replace(/\s+/g, '')
// Deterministic cache key using only validated mTLS identity
const cacheKey = `profile:${crypto.createHash('sha256').update(normalizedDn).digest('hex')}`
// Example: retrieve or compute profile
const profile = await cache.get(cacheKey)
if (profile) {
return response.send(profile)
}
const data = await Database.from('profiles').where('dn', normalizedDn).first()
await cache.put(cacheKey, data, 300)
return response.send(data)
}
}
2. Exclude mutable inputs from cache key when mTLS is used
Avoid using query parameters or mutable headers in cache keys unless they are explicitly validated and constrained. Instead, derive keys from mTLS-bound identities and safe static segments.
import { HttpContextContract } from '@ioc:AdonisJS/Core/HttpContext'
export default class ReportsController {
public async generateReport({ request, response }: HttpContextContract) {
const certDn = request.header('x-client-cert-dn')
if (!certDn) {
response.status(400).send({ error: 'Client certificate required' })
return
}
// Validate and constrain values that influence behavior
const safeLocale = request.qs().locale || 'en'
if (!['en', 'fr', 'de'].includes(safeLocale)) {
response.status(400).send({ error: 'Unsupported locale' })
return
}
// Build cache key from validated mTLS identity + safe locale
const key = `report:${certDn}:${safeLocale}`
const cached = await cache.get(key)
if (cached) {
return response.send(cached)
}
const report = await generateComplexReport(safeLocale, certDn)
await cache.put(key, report, 60)
return response.send(report)
}
}
3. Enforce strict header validation and avoid passing untrusted inputs to cache layers
Configure your caching layer to key only on server-controlled values. In AdonisJS, you can encapsulate cache key construction in a service that validates mTLS-derived attributes and rejects any unexpected inputs.
import { HttpContextContract } from '@ioc:AdonisJS/Core/HttpContext'
class CacheKeyBuilder {
static fromRequest(request: HttpContextContract['request']) {
const certDn = request.header('x-client-cert-dn')
if (!certDn || !/^CN=[A-Za-z0-9_\-\.]+,O=Example$/i.test(certDn)) {
throw new Error('Invalid certificate DN')
}
// Only use validated DN; ignore query/headers that could poison cache
return `v1:resource:${certDn.trim()}`
}
}
export default CacheKeyBuilder
4. Middleware to normalize and validate mTLS inputs before route handling
Use an AdonisJS middleware to ensure that cache-affecting inputs are validated and normalized early, preventing accidental use of raw values.
import { HttpContextContract } from '@ioc:AdonisJS/Core/HttpContext'
export default class MtlsCacheNormalizer {
public async handle(ctx: HttpContextContract, next: () => Promise) {
const certDn = ctx.request.header('x-client-cert-dn')
if (certDn) {
// Normalize once for the request lifetime
ctx.request['__normalizedDn'] = certDn.trim().toLowerCase().replace(/\s+/g, '')
}
await next()
}
}