Prototype Pollution in Adonisjs with Jwt Tokens
Prototype Pollution in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Prototype pollution in AdonisJS can intersect with JWT token handling when user-controlled data from token payloads is merged into objects that later influence behavior or serialization. A JWT issued by an identity provider or derived from application logic may include dynamic claims that an endpoint merges into configuration objects, request context, or response helpers. If the application uses shallow merges (e.g., Object.assign or lodash merge) on token-derived objects without validating or copying properties, an attacker can inject keys such as __proto__, constructor, or prototype fields that mutate shared prototypes across requests.
In AdonisJS, this commonly occurs when route middleware or auth guards consume decoded JWT payloads and pass them to services that later construct responses or query databases. For example, a payload containing { "role": "user", "__proto__": { "isAdmin": true } } can, when merged into an options object, affect subsequent object instantiation or permission checks. Because AdonisJS often serializes objects to JSON for logging or error reporting, a polluted prototype can lead to unintended data exposure, such as leaking sensitive fields attached to Object.prototype or altering toJSON behavior. The risk is amplified when tokens carry user-supplied claims (e.g., from OAuth or custom extensions) that the application naively trusts and merges without deep cloning or schema validation.
An attacker may also exploit prototype pollution to tamper with rate limiter state, configuration caches, or security checks that rely on shared objects across requests. If the JWT is used to derive identifiers passed to database queries or authorization helpers, a modified prototype key can change equality checks or serialization output, indirectly enabling privilege escalation or data exposure. Because the JWT signature may still validate (if the server does not enforce strict claim validation), the malicious payload appears legitimate, bypassing superficial integrity checks. The effective attack surface therefore includes any code path that deserializes a token, merges its claims into mutable structures, and later uses those structures in security-sensitive decisions or object construction within the AdonisJS runtime.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on strict validation, deep cloning, and avoiding unsafe merges of JWT payload data. Always verify issuer, audience, and required claims before using token contents. Use a validation layer that whitelists expected fields and rejects unknown keys, especially __proto__, constructor, and prototype. Prefer immutable operations and explicit property extraction instead of merging entire payloads.
Example 1: Safe JWT verification and claim extraction in a route middleware
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema, rules } from '@ioc:Adonis/Core/Validator'
import { authenticator } from 'otplib'
const jwtClaimSchema = schema.create({
token: schema.string.optional(),
payload: schema.object().shape({
sub: schema.string.optional(),
email: schema.string.optional(),
role: schema.enum(['user', 'admin']).optional(),
// explicitly allow only expected claims
}).optional(),
})
export default class AuthMiddleware {
public async handle({ request, response, auth }: HttpContextContract, next: () => Promise) {
const token = request.header('authorization')?.replace('Bearer ', '')
if (!token) {
return response.unauthorized('Missing token')
}
// Verify and decode safely; do not merge entire payload
const { payload } = await auth.use('jwt').verifyAndDecode(token)
// Validate against a strict schema to discard unexpected claims
const validated = await validator.validate({
schema: jwtClaimSchema,
data: { token, payload },
})
// Explicitly extract needed fields; avoid Object.assign on payload
const { sub, email, role } = validated.payload || {}
request.authUser = { id: sub, email, role }
await next()
},
}
Example 2: Avoiding prototype pollution when constructing response or context objects
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class UserController {
public async profile({ request, auth }: HttpContextContract) {
const tokenUser = auth.user
// Do NOT merge request body or payload directly into prototypes
const safeData = {
id: tokenUser.id,
email: tokenUser.email,
role: tokenUser.role,
}
// Explicitly build response without mutating shared objects
return {
user: safeData,
meta: { generatedAt: new Date().toISOString() },
}
},
}
Example 3: Using deep clone when you must merge token-derived data
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { cloneDeep } from 'lodash'
export default class SettingsController {
public async update({ request, auth }: HttpContextContract) {
const body = request.only(['theme', 'notifications'])
const tokenUser = auth.user
// Clone before merge to prevent prototype modification
const merged = cloneDeep({
id: tokenUser.id,
settings: {},
})
merged.settings = { ...body }
// Use merged safely
await tokenUser.merge({ settings: merged.settings }).save()
},
}