Header Injection in Adonisjs with Mutual Tls
Header Injection in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Header Injection occurs when user-controlled data is reflected into HTTP response headers without validation or encoding. In AdonisJS, this commonly arises when developers forward incoming headers or query parameters into downstream requests using libraries such as @adonisjs/axios or native Node HTTP clients. When Mutual TLS (mTLS) is enabled, the server presents a client certificate during the TLS handshake and may also inspect client certificates for authorization. While mTLS strengthens transport-layer identity, it does not inherently protect against application-layer header manipulation; in some configurations it can inadvertently encourage trust in request metadata, including headers derived from or influenced by the client certificate or mTLS-specific request attributes.
With mTLS, AdonisJS applications often terminate TLS at the reverse proxy or load balancer and then forward requests internally over HTTPS to the Node server. If the application forwards the original request’s headers—including headers derived from mTLS metadata such as selected cipher suite, certificate fingerprint, or custom headers injected by the mTLS layer—into outbound calls, an attacker may inject newline characters (%0A, %0D) to append or override headers like x-forwarded-proto, x-client-verify, or custom authorization headers. Because outbound requests may be treated as trusted due to mTLS, injected headers can manipulate routing, authentication decisions, or logging behavior. For example, an attacker could set x-forwarded-proto: https to influence URL generation or bypass protocol checks that rely on header values rather than the verified mTLS certificate state.
Consider an AdonisJS route that initiates an outbound request using user-supplied input taken from headers:
const { request } = useRequest()
const axios = use('axios')
Route.get('/api/external', async ({ request, response }) => {
const userAgent = request.header('x-custom-agent')
const url = 'https://upstream.example.com/info'
try {
const res = await axios.get(url, {
headers: {
'x-forwarded-for': request.ip(),
'x-custom-agent': userAgent || 'default-agent'
}
})
response.send(res.data)
} catch (error) {
response.status(502).send({ error: 'upstream failure' })
}
})
If x-custom-agent or any other header is attacker-controlled and contains newline sequences, it can inject additional headers such as x-forwarded-proto: http or x-injected: true in the outbound request. Although mTLS ensures the client presented a valid certificate, the application logic still treats the header value as safe. This mismatch between transport assurance and input validation creates a bypass where header manipulation affects behavior, potentially altering request semantics when proxies or upstream services rely on injected headers.
AdonisJS’s built-in body parsers and middleware do not automatically sanitize headers used in outbound requests. Developers must explicitly validate, denylist newline characters, and avoid directly forwarding untrusted headers. mTLS should be treated as a complementary control for identity and encryption, not a substitute for output encoding and strict header validation in application code.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on strict header validation, avoiding the forwarding of untrusted inputs, and explicitly handling mTLS metadata as separate from application data. Do not assume that mTLS-authenticated clients are safe to influence routing or authorization via headers; always validate and sanitize any value that will be reflected into request or response headers.
1. Validate and sanitize user-supplied header values
Never pass raw user input into outbound headers. Strip or reject newline characters and enforce allowlists where possible.
const sanitizeHeader = (value) => {
if (typeof value !== 'string') return ''
// Reject newlines and carriage returns to prevent header injection
if (/[\r\n]/.test(value)) return ''
return value.trim()
}
Route.get('/api/external', async ({ request, response }) => {
const userAgent = sanitizeHeader(request.header('x-custom-agent'))
const fallbackAgent = 'default-agent'
const safeAgent = userAgent || fallbackAgent
const url = 'https://upstream.example.com/info'
try {
const res = await axios.get(url, {
headers: {
'x-forwarded-for': request.ip(),
'x-custom-agent': safeAgent
}
})
response.send(res.data)
} catch (error) {
response.status(502).send({ error: 'upstream failure' })
}
})
2. Do not forward mTLS metadata as user-controlled headers
When terminating mTLS at the edge, avoid echoing certificate-derived headers (e.g., SSL client certificate fields) into outbound requests without strict validation. Instead, map only the claims you explicitly trust.
// Example: explicitly extract and validate a specific certificate claim
const getVerifiedTenant = (cert) => {
const tenantId = cert?.extensions?.find(e => e.oid === '1.3.6.1.4.1.12345')?.value
if (!tenantId) return null
// Validate format (e.g., UUID)
if (!/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(tenantId)) return null
return tenantId
}
Route.get('/api/tenant-data', async ({ request, response }) => {
const clientCert = request.$ssl?.clientCert // assume mTLS provides this in request context
const tenantId = getVerifiedTenant(clientCert)
if (!tenantId) return response.status(400).send({ error: 'invalid certificate' })
const url = `https://api.example.com/tenant/${encodeURIComponent(tenantId)}/info`
try {
const res = await axios.get(url)
response.send(res.data)
} catch (error) {
response.status(502).send({ error: 'upstream failure' })
}
})
3. Enforce HTTPS and mTLS verification for outbound calls
When making outbound HTTPS calls, explicitly set TLS options to require server verification and, if needed, client certificates for upstream services that also use mTLS.
const httpsAgent = new https.Agent({
cert: fs.readFileSync('/path/client-cert.pem'),
key: fs.readFileSync('/path/client-key.pem'),
ca: fs.readFileSync('/path/ca-bundle.pem'),
rejectUnauthorized: true
})
Route.get('/api/secure-upstream', async ({ request, response }) => {
const url = 'https://upstream.example.com/secure'
try {
const res = await axios.get(url, {
httpsAgent,
headers: {
// Only include safe, validated headers
'x-request-id': request.id()
}
})
response.send(res.data)
} catch (error) {
response.status(502).send({ error: 'upstream failure' })
}
})
4. Use framework-level middleware to normalize mTLS-derived data
Create a middleware layer that maps verified mTLS fields into request context rather than allowing raw header passthrough. This keeps transport identity separate from application-level headers.
// provider/app.js or a dedicated start/hook file
'use strict'
const HttpsProxyAgent = require('https-proxy-agent')
module.exports = {
async ready() {
// Example hook to attach verified mTLS info to request context
const mTLSMiddleware = async (ctx, next) => {
const clientCert = ctx.$ssl?.clientCert
ctx.mtls = {
verified: !!clientCert,
// Derive only explicitly validated attributes
tenantId: getVerifiedTenant(clientCert)
}
await next()
}
Server.use(mTLSMiddleware)
}
}