Side Channel Attack in Adonisjs with Jwt Tokens
Side Channel Attack in Adonisjs with Jwt Tokens — how this combination creates or exposes the vulnerability
A side channel attack in AdonisJS when using JWT tokens occurs when an attacker infers sensitive information from indirect behavior rather than from a direct vulnerability in JWT verification itself. Timing differences in token validation, error messages, or rate-limiting responses can reveal whether a token is valid, which user it belongs to, or whether a brute-force or dictionary attack is succeeding.
In AdonisJS, JWT handling is often implemented via the jwt provider in start/app.js and token generation in authentication flows. If token validation logic does not use constant-time comparison patterns, subtle timing variations can be observed. For example, an endpoint that returns a 401 with a message like “Invalid token” for malformed tokens and a 403 with “Invalid signature” for valid-format-but-invalid tokens enables an attacker to distinguish between a syntactically correct token and one that fails verification due to signature mismatch.
Consider an endpoint that loads a user by ID extracted from a JWT payload without consistent timing and error handling. An attacker can send many token guesses; if user lookup timing differs based on token validity or user existence, the attacker can infer relationships between tokens and user accounts. AdonisJS does not introduce this risk inherently, but application code that branches on token validity or user presence creates a practical side channel.
Another vector arises from unauthenticated endpoints that process JWTs in non-constant time when rejecting tokens, or when returning different error paths for missing versus malformed tokens. For instance, failing early on missing tokens versus performing partial validation on present but malformed tokens can expose whether a token was provided at all. Similarly, responses that include stack traces or verbose validation details for development environments can disclose which checks failed and where, aiding an attacker in refining guesses.
LLM/AI Security checks are relevant because an exposed endpoint that echoes token-related behavior or validation steps could be probed by automated prompts attempting to extract validation logic or timing characteristics. Although middleBrick does not fix these issues, its findings can highlight inconsistent error handling or timing-sensitive routes that should be reviewed to reduce side channel leakage.
To mitigate, ensure token validation paths are consistent in timing and do not leak user existence. Standardize error responses and avoid branching logic on token validity details. Where possible, perform constant-time comparisons for any derived values and apply uniform rate limiting regardless of token validity to obscure timing signals.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on making token validation timing consistent and standardizing error handling in AdonisJS. Below are concrete code examples that demonstrate secure patterns.
1. Use a constant-time comparison for token validation outcomes where possible, and ensure error paths do not reveal token validity.
import { BaseMiddleware } from '@ioc:Adonis/Core/Middleware'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Jwt } from '@ioc:Adonis/Addons/Jwt'
export default class JwtAuthMiddleware implements BaseMiddleware {
public async handle({ request, response, session }: HttpContextContract, next: () => Promise) {
const authHeader = request.headers().authorization
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
// Perform a dummy verification to keep timing consistent
let isValid = false
if (token) {
try {
await Jwt.verify(token, 'your-secret-key')
isValid = true
} catch (error) {
// Log internally, but do not reveal details
console.error('JWT verification error', error)
isValid = false
}
}
// Always advance with the same structure
await next()
// If not valid after verification, respond uniformly
if (!isValid) {
return response.unauthorized({ message: 'Unauthorized' })
}
}
}
2. Centralize token parsing and verification in a service to avoid branching on error types in route handlers.
// services/jwt.ts
import { Jwt } from '@ioc:Adonis/Addons/Jwt'
export class JwtService {
public static async verifyToken(token: string) {
try {
return { valid: true, payload: await Jwt.verify(token, 'your-secret-key') }
} catch (error) {
return { valid: false, payload: null }
}
}
}
// controllers/AuthController.ts
import JwtService from 'App/Services/jwt'
export default class AuthController {
public async login({ request, auth }: HttpContextContract) {
const { email, password } = request.body()
const user = await User.findBy('email', email)
if (!user || !(await verifyPassword(password, user.password))) {
return response.unauthorized({ message: 'Unauthorized' })
}
const token = jwt.sign({ sub: user.id }, 'your-secret-key', { expiresIn: '1h' })
return { token }
}
public async protected({ request, response }: HttpContextContract) {
const authHeader = request.headers().authorization
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
const { valid, payload } = await JwtService.verifyToken(token || '')
if (!valid || !payload) {
return response.unauthorized({ message: 'Unauthorized' })
}
// Proceed with user-specific logic using payload.sub
return response.ok({ userId: payload.sub })
}
}
3. Apply uniform rate limiting on authentication endpoints regardless of token validity to obscure timing-based inference.
import Route from '@ioc:Adonis/Core/Route'
import { rateLimiter } from '@ioc:Adonis/Addons/RateLimiter'
Route.post('/login', async ({ request, response }) => {
const { email } = request.body()
// Perform login logic...
return response.ok({ token: 'example' })
}).rateLimit({
endpoint: 'login',
pace: 5, // 5 requests per minute
capacity: 10,
})
4. Avoid exposing environment-specific details in responses. Ensure development and production error formats are aligned for token-related failures.
// start/hooks.ts
import { HttpServerHook } from '@adonisjs/core/types'
const handleError: HttpServerHook['onError'] = (error, { response }) => {
if (response.status === 401) {
response.status(401).json({ message: 'Unauthorized' })
}
// Keep other responses consistent
}
By standardizing validation timing and error responses, you reduce the risk that timing or behavioral differences expose token validity or user information to an attacker.