Stack Overflow in Adonisjs with Mutual Tls
Stack Overflow in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
A Stack Overflow in an AdonisJS application using Mutual TLS (mTLS) typically occurs when recursive request handling or uncontrolled middleware interactions cause the call stack to grow beyond the runtime limit. With mTLS enabled, the server validates client certificates on each incoming request. If the application logic or middleware inadvertently re-triggers requests to internal endpoints (for example, during token refresh, profile enrichment, or delegated authorization), the repeated TLS handshakes and request allocations can compound stack usage. This is especially risky when mTLS verification is performed synchronously in middleware that does not guard against re-entry, because each verification step may push additional frames onto the stack.
Simultaneously, the mTLS setup can expose subtle configuration issues that amplify the impact. For example, if certificate validation success is used to eagerly load user data or session state without rate-limiting or concurrency guards, a malicious actor could craft many simultaneous mTLS-authenticated requests that each trigger recursive internal calls. The combination of AdonisJS route handlers, IoC container resolutions, and mTLS verification callbacks can create deep call chains that lead to stack exhaustion. Moreover, if the application introspects client certificate details (such as Common Name or SANs) and uses them to dynamically import modules or generate routes, a malformed certificate or a crafted chain can cause recursive resolution logic, further deepening the stack.
Consider an endpoint that, upon successful mTLS authentication, calls an internal service function that itself triggers another authenticated request back to the same route (for instance, a webhook or a delegated token refresh). Because mTLS is enforced on the entry point, each recursion incurs additional stack overhead for TLS session state and request context. Without proper limits or async boundary management, this recursion can overflow the stack. The vulnerability is not in mTLS itself but in how the application integrates mTLS checks with request handling patterns that lack recursion protection or depth-limiting strategies.
An example pattern that can lead to this scenario in AdonisJS is a middleware that eagerly loads a user profile via an internal recursive call after verifying the client certificate:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class MtlsMiddleware {
public async handle(ctx: HttpContextContract, next: () => Promise) {
const cert = ctx.request.ssl()
if (!cert || !cert.issuer) {
ctx.response.status(400).send('Client certificate required')
return
}
// Risky: recursive profile resolution based on cert fields
const profile = await this.resolveProfileRecursively(cert.subject)
ctx.auth.user = profile
await next()
}
private async resolveProfileRecursively(dn: string, depth = 0): Promise {
if (depth > 10) throw new Error('Depth limit exceeded')
const user = await User.findBy('dn', dn)
if (user && user.parentDn) {
return this.resolveProfileRecursively(user.parentDn, depth + 1)
}
return user
}
}
If an attacker can influence the certificate chain or cause repeated internal re-resolution, the recursive calls may grow quickly and contribute to stack exhaustion under high concurrency. This demonstrates why mTLS integrations in AdonisJS must include strict depth limits, memoization, and asynchronous boundaries to prevent Stack Overflow scenarios.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
To mitigate Stack Overflow risks and ensure robust mTLS handling in AdonisJS, apply defensive patterns that limit recursion, isolate synchronous work from asynchronous flows, and validate inputs before acting on certificate metadata. The following code examples demonstrate concrete fixes.
1. Use iterative resolution instead of recursion when traversing certificate-based hierarchies:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class MtlsMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise) { const cert = ctx.request.ssl() if (!cert || !cert.issuer) { ctx.response.status(400).send('Client certificate required') return } const profile = await this.resolveProfileIteratively(cert.subject) ctx.auth.user = profile await next() } private async resolveProfileIteratively(dn: string): Promise { const visited = new Set () let current = dn while (current && !visited.has(current)) { visited.add(current) const user = await User.findBy('dn', current) if (!user || !user.parentDn || user.parentDn === current) { return user } current = user.parentDn } return null } } 2. Enforce concurrency and rate limits around mTLS-authenticated request handling to prevent resource exhaustion:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { RateLimiter } from '@ioc:Adonis/Addons/RateLimiter' export default class MtlsRateLimitMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise) { const cert = ctx.request.ssl() if (!cert) { ctx.response.status(400).send('Client certificate required') return } const limiter = new RateLimiter({ duration: 60, limit: 30 }) const key = `mtls:${cert.subject}` const allowed = await limiter.check(key, 1) if (!allowed) { ctx.response.status(429).send('Too many requests') return } await next() } } 3. Validate and sanitize certificate fields before using them in dynamic imports or route generation to avoid recursive or uncontrolled resolution:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class SafeMtlsMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise) { const cert = ctx.request.ssl() if (!cert) { ctx.response.status(400).send('Client certificate required') return } const subject = this.sanitizeDn(cert.subject) if (!subject) { ctx.response.status(400).send('Invalid certificate subject') return } const profile = await User.findBy('dn', subject) if (!profile) { ctx.response.status(403).send('Unauthorized certificate') return } ctx.auth.user = profile await next() } private sanitizeDn(dn: string): string | null { // Basic DN sanitization to prevent path traversal or injection const safe = dn.replace(/[^A-Za-z0-9=,\s+\-]/g, '') return safe.length > 0 && safe.length <= 512 ? safe : null } } 4. Prefer asynchronous guards and avoid performing heavy or recursive logic inside synchronous middleware when possible. For example, defer profile resolution to a scheduled job or a lightweight lookup that does not recurse:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class AsyncMtlsMiddleware { public async handle(ctx: HttpContextContract, next: () => Promise) { const cert = ctx.request.ssl() if (!cert) { ctx.response.status(400).send('Client certificate required') return } // Store certificate for later async processing ctx.auth.certFingerprint = cert.fingerprint await next() } } // Later in a controller or event listener: import { Job } from '@ioc:Adonis/Async/Job' export class ResolveProfileJob extends Job { public constructor(private certFingerprint: string) { super() } public async handle() { const profile = await this.lookupProfileByFingerprint(this.certFingerprint) // store or publish as needed } private async lookupProfileByFingerprint(fp: string): Promise { // non-recursive, cached lookup return ProfileCache.get(fp) } }