Timing Attack in Adonisjs with Jwt Tokens
Timing Attack in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
In AdonisJS applications that use JSON Web Tokens (JWT), timing attacks can occur during token verification when operations do not execute in constant time. JWT verification typically involves signature validation and, optionally, payload claims checks such as exp (expiration) and iss (issuer). If the comparison of the provided signature against the expected signature is performed using a standard string equality check, an attacker can measure response times to progressively infer information about the valid signature. This is a classic timing attack vector: by submitting modified tokens and observing whether verification takes slightly longer, an attacker can learn which bytes of the signature match, eventually recovering the full signature or private key material depending on the algorithm used.
AdonisJS commonly uses the jsonwebtoken package under the hood for handling JWTs. When verifying tokens with asymmetric algorithms such as RS256, the server must use the corresponding public key. If the public key is retrieved based on a key identifier (e.g., a kid header) and the lookup or cryptographic operation is not constant-time, an attacker may also infer whether a given kid is valid. Furthermore, if the application performs additional checks on token claims in an order that depends on their presence or values, this can introduce further timing variability. For example, checking for the exp claim only if the token header is valid can allow an attacker to distinguish between malformed tokens and valid-format tokens based on response time.
Even in unauthenticated attack scenarios—where no credentials are required—JWT verification endpoints can be probed to assess whether the server exhibits timing differences across valid and invalid tokens. Because middleBrick scans the unauthenticated attack surface and includes checks for authentication and input validation timing behaviors, it can detect potential timing-related inconsistencies in JWT verification logic. A scanner might observe that certain invalid tokens cause slower or faster responses than others, indicating that the implementation is not uniformly handling all verification steps.
Real-world attack patterns related to this issue are aligned with the broader OWASP API Top 10 category for Security Misconfiguration and Authentication weaknesses. For instance, if an API endpoint accepts a JWT in an Authorization header and does not enforce strict input normalization before verification, an attacker could submit tokens with varying header structures or claim sets to measure differences. While middleBrick does not attempt to exploit or break cryptography, it can surface findings related to inconsistent handling of tokens, encouraging developers to review verification logic for timing safety.
To illustrate a typical JWT verification flow in AdonisJS that may be susceptible if not carefully implemented, consider the following example. This code verifies a token but does not explicitly address constant-time practices for comparison operations:
import { BaseController } from '@ioc:Adonis/Core/Controller'
import jwt from 'jsonwebtoken'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class AuthController extends BaseController {
public async verify({ request, response }: HttpContextContract) {
const token = request.header('authorization')?.replace('Bearer ', '')
if (!token) {
return response.unauthorized()
}
try {
const decoded = jwt.verify(token, 'YOUR_PUBLIC_KEY_OR_SECRET', {
algorithms: ['RS256'],
})
return response.ok(decoded)
} catch (error) {
return response.badRequest({ error: 'Invalid token' })
}
}
}
In this example, the call to jwt.verify may perform comparisons internally that are not constant-time, depending on the underlying library and configuration. If the public key is fetched dynamically and the lookup time varies, or if the error handling branches differ based on the type of verification failure, an attacker may be able to distinguish between different failure modes. Using middleBrick's LLM/AI Security and authentication checks can help identify such inconsistencies by analyzing the observable behavior of the endpoint across many requests.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring that JWT verification proceeds in a consistent amount of time regardless of token validity. The primary goal is to avoid branching logic that depends on secret or key material and to standardize error responses. In AdonisJS, this can be achieved by normalizing inputs, using fixed-time comparison utilities where applicable, and ensuring that all verification paths take the same amount of time.
One approach is to perform a constant-time verification step before proceeding with full claim validation. While the jsonwebtoken library does not expose a low-level constant-time verification function directly, you can mitigate timing differences by ensuring that public key retrieval and token parsing do not leak information through timing channels. For example, always load the public key or secret regardless of the kid value and use it in verification, rather than conditionally looking it up based on the header.
Below is a revised example that demonstrates safer handling in AdonisJS. It includes a fixed public key reference and avoids early exits based on malformed tokens that could leak timing information through different code paths:
import { BaseController } from '@ioc:Adonis/Core/Controller'
import jwt from 'jsonwebtoken'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
// Use a fixed key or a key resolver that does not vary by request
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`
export default class AuthController extends BaseController {
public async verify({ request, response }: HttpContextContract) {
const authHeader = request.header('authorization')
const token = typeof authHeader === 'string' && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
// Normalize missing token case to avoid branching on token presence
const safeToken = token || ''
try {
// Always verify with a fixed key and constant-time safe options
const decoded = jwt.verify(safeToken, PUBLIC_KEY, {
algorithms: ['RS256'],
clockTolerance: 10,
})
return response.ok(decoded)
} catch (error) {
// Use a generic error response to avoid signaling specific failure reasons
return response.badRequest({ error: 'Invalid token' })
}
}
}
Additionally, consider using a library that supports constant-time string comparison for any custom validation logic you implement. For example, when comparing values derived from the token payload, use a utility that avoids early exit on mismatch:
function safeCompare(a: string, b: string): boolean {
if (a.length !== b.length) {
return false
}
let result = 0
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return result === 0
}
While this pattern is more relevant for custom claim checks, it reinforces the principle that all comparisons should execute in constant time. Using middleBrick's Pro plan with continuous monitoring and GitHub Action integration can help ensure that any timing regressions introduced by future changes are caught before deployment. The MCP Server can also be used to scan APIs directly from your IDE, providing early feedback during development.