HIGH memory leakadonisjsjwt tokens

Memory Leak in Adonisjs with Jwt Tokens

Memory Leak in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability

A memory leak in an AdonisJS application that uses JWT tokens typically arises when token payload data or authentication state is retained in server-side structures across requests. Unlike session-based auth, JWT is often designed to be stateless, but developers sometimes store per-request or per-user data in closures, event emitters, caches, or long-lived objects without cleanup. In AdonisJS, this can occur when middleware attaches decoded tokens to the request context and that context is retained indirectly—for example, attaching the token payload to a global map keyed by user identifier and never removing entries. Over time, as requests accumulate, these retained objects increase memory usage, leading to a gradual memory leak.

Another common pattern is in token refresh or revocation logic where a list of invalidated tokens or blacklisted identifiers grows indefinitely. If the blacklist is kept in memory (e.g., a simple array or object) and not persisted to a bounded store or periodically flushed, it will consume increasing memory. This is particularly risky when token payloads include large custom claims or when the application runs in a long-running process without process recycling. Because AdonisJS runs on Node.js, V8 heap pressure can trigger more frequent garbage collections, increased latency, and eventually out-of-memory crashes under sustained load.

The interaction with JWT tokens also exposes patterns where asynchronous operations hold references to request-scoped data. For instance, if a token validation hook spawns async tasks (such as emitting events or scheduling jobs) that capture the request context or decoded token, those tasks can keep objects alive longer than intended. This is exacerbated if the application creates closures over token claims for background processing without explicitly nullifying references after use. Since the scan checks for SSRF and other runtime behaviors, memory growth patterns tied to token handling can correlate with insecure deserialization or improper data handling flagged in other checks.

From a scanning perspective, middleBrick detects indicators that suggest unbounded in-memory growth related to token handling, such as missing cleanup in authentication middleware, large or unbounded caches, and absence of TTL mechanisms on server-side structures that mirror token state. These findings do not imply that JWT itself is flawed, but that implementation choices in AdonisJS—how token payloads are stored, referenced, and released—can introduce memory leak risks that degrade stability over time.

Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes

To remediate memory leaks when using JWT tokens in AdonisJS, focus on avoiding long-lived references to token payloads, bounding caches, and ensuring cleanup of server-side state. Below are concrete code examples demonstrating safe patterns.

1. Stateless validation without attaching large payloads

Keep token validation stateless. Verify the token on each request without storing the payload in long-lived structures.

import { BaseMiddleware } from '@ioc:Adonis/Core/Middleware'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import jwt from 'jsonwebtoken'

export default class JwtAuthMiddleware implements BaseMiddleware {
  public async handle({ request, auth }: HttpContextContract, next: () => Promise) {
    const token = request.headers().authorization?.replace('Bearer ', '')
    if (!token) {
      throw new Error('Unauthorized')
    }
    // Verify token without attaching decoded payload to request beyond what's needed
    const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { sub: string }
    // Use only necessary claims; avoid storing entire payload
    request.authUser = { id: decoded.sub }
    await next()
  }
}

2. Bounded cache for token blacklist with TTL

If you maintain a blacklist for revoked tokens, use a bounded data structure with TTL to prevent unbounded growth.

import { Redis } from '@ioc:Adonis/Addons/Redis'

class TokenBlacklist {
  private redis: Redis
  constructor(redis: Redis) {
    this.redis = redis
  }

  async add(jti: string, exp: number) {
    // Store with TTL matching remaining token lifetime
    await this.redis.set(`blacklist:${jti}`, 'revoked', 'EX', Math.max(60, exp - Math.floor(Date.now() / 1000)))
  }

  async has(jti: string): Promise {
    return (await this.redis.get(`blacklist:${jti}`)) === 'revoked'
  }
}

// Usage in a route or middleware
const blacklist = new TokenBlacklist(redis)
await blacklist.add(payload.jti, payload.exp)
const revoked = await blacklist.has(payload.jti)

3. Avoid closures capturing token context in async tasks

When scheduling jobs or emitting events, do not close over the full token payload. Pass only necessary identifiers.

import { Queue } from '@ioc:Adonis/Addons/Queue')

interface JobPayload {
  userId: string
  // Do not include token claims or large payloads
}

await Queue.add('send:notification', {
  userId: user.id,
} as JobPayload)

// Worker
Queue.boot(() => {
  Queue.process('send:notification', async (job) => {
    const { userId } = job.data
    // Use userId to fetch fresh data
  })
})

4. Periodic cleanup of in-memory structures

If you must keep server-side mappings, implement a cleanup routine with TTL.

class SessionCache {
  private cache: Map = new Map()

  set(key: string, value: any, ttlMs: number) {
    this.cache.set(key, { value, expiry: Date.now() + ttlMs })
  }

  get(key: string) {
    const entry = this.cache.get(key)
    if (!entry) return undefined
    if (Date.now() > entry.expiry) {
      this.cache.delete(key)
      return undefined
    }
    return entry.value
  }

  // Call cleanup periodically, e.g., via setInterval or a scheduled task
  cleanup() {
    for (const [key, entry] of this.cache.entries()) {
      if (Date.now() > entry.expiry) {
        this.cache.delete(key)
      }
    }
  }
}

5. Use HTTP-only, secure cookies for refresh tokens instead of storing in memory

For refresh token flows, prefer encrypted HTTP-only cookies and short-lived access tokens to minimize server-side state.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export async function refreshToken({ request, response }: HttpContextContract) {
  const cookie = request.cookies.get('refresh_token')
  if (!cookie) {
    throw new Error('Invalid request')
  }
  // Verify and rotate refresh token, set new HTTP-only cookie
  const payload = verifyRefreshToken(cookie)
  const newAccessToken = generateAccessToken({ sub: payload.sub })
  response.cookie('refresh_token', encrypt(payload), {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/auth/refresh',
  })
  return { accessToken: newAccessToken }
}

Frequently Asked Questions

Can JWT tokens themselves cause memory leaks in AdonisJS?
JWT tokens are passive data structures; they do not cause leaks by themselves. Leaks occur when server-side code retains references to decoded payloads, caches, or async closures without cleanup. Proper stateless validation and bounded storage prevent issues.
Does middleBrick fix memory leaks related to JWT tokens in AdonisJS?
middleBrick detects and reports indicators of unbounded memory growth related to token handling and provides remediation guidance. It does not fix or patch; developers must apply the recommended code changes.