Ssrf Server Side in Adonisjs with Jwt Tokens
Ssrf Server Side in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Server Side Request Forgery (SSRF) in an AdonisJS application that uses JWT tokens for authentication can occur when an endpoint that validates and acts upon a JWT also performs arbitrary HTTP requests based on attacker-controlled input. Because the JWT carries identity and potentially elevated scopes, the backend may trust input that originates from an authenticated context and initiate requests to internal or external hosts that would otherwise be unreachable. Typical patterns include a user-supplied URL or host header being passed to an HTTP client after the JWT is verified, allowing an authenticated user to direct the server to interact with internal metadata services, cloud instance metadata endpoints, or internal APIs that trust the loopback interface.
In AdonisJS, this often maps to routes guarded by JWT middleware where the route handler parses the token and then uses parameters such as url, host, or file to perform outbound calls. If input validation is limited to presence checks and the JWT is assumed to imply safety, an authenticated attacker can abuse this to perform SSRF against internal services, bypassing firewall rules that would normally block unauthenticated access. For example, an endpoint meant to proxy a report or fetch a user avatar can be coerced into probing the metadata service at http://169.254.169.254 or internal Kubernetes endpoints when the JWT identifies an admin or service account.
The risk is compounded when the JWT contains scopes or roles that authorize sensitive operations, and the application logic does not enforce additional checks on the target of the outbound request. AdonisJS does not inherently prevent SSRF; the framework relies on developer-side validation and network controls. Without strict allowlists, hostname resolution controls, and network segmentation, an SSRF enabled by a trusted JWT-authenticated session can lead to internal service enumeration, cloud metadata exfiltration, or pivot into adjacent environments.
Middleware that verifies JWTs and attaches user context to the request should never skip validation of outbound destinations. Even with a valid token, the application must treat the request body, query parameters, and headers as untrusted. Combining JWT-based authorization with unchecked HTTP client configuration creates a pathway where authenticated requests can be weaponized to interact with unintended internal endpoints, illustrating why SSRF considerations must be applied uniformly regardless of authentication state.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
To mitigate SSRF in AdonisJS when JWT tokens are used, enforce strict input validation and network controls independently of authentication. Treat validated JWT claims as identity, not as authorization to access arbitrary resources. Apply allowlists for destinations, disable unnecessary protocols, and avoid forwarding user-controlled data directly to outbound HTTP clients.
Example JWT middleware and route handling in AdonisJS (with remediation)
Use AdonisJS middleware to verify JWTs and attach a user payload, but ensure that any user-controlled input used for outbound requests is validated against a strict allowlist.
// start/kernel.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema, rules } from '@ioc:Adonis/Core/Validator'
// Example route-specific validator
const reportSchema = schema.create({
url: stringSchema({}, [
rules.url({ allowedProtocols: ['https'] }), // restrict to HTTPS
rules.hostname({ allowPrivate: false }), // prevent private IPs/localhost
rules.regex({ pattern: /^https:\/\/reports\.example\.com\/v1\/.+/ }), // strict prefix allowlist
]),
token: stringSchema({}, [rules.alpha()]), // non-opaque example constraint
})
export default class ReportsController {
public async showReport({ request, response, auth }: HttpContextContract) {
const payload = request.validate({ schema: reportSchema })
const user = await auth.authenticate() // JWT verified by auth provider
// Ensure the JWT scopes authorize this operation
if (!user.scopes.includes('reports:fetch')) {
return response.forbidden({ message: 'Insufficient scope' })
}
// Safe outbound call with explicit whitelisting
const safeUrl = new URL(payload.url)
if (!safeUrl.hostname.endsWith('.example.com')) {
return response.badRequest({ message: 'Destination not allowed' })
}
// Use a configured HTTP client with disabled protocols like file:// and gopher://
const ReportHttp = use('App/Services/ReportHttp')
const data = await ReportHttp.get(safeUrl.toString(), {
headers: { Authorization: `Bearer ${user.token}` },
})
return response.ok(data)
}
}
Configure the HTTP client to restrict protocols and hosts. For example, using got or axios with explicit settings:
// app/Services/ReportHttp.ts
import got from 'got'
class ReportHttp {
async get(url: string, options = {}) {
return got(url, {
protocol: 'https:',
hostname: new URL(url).hostname,
// Disable protocols that should never be used
handlers: {
// Ensure only HTTPS is used; reject file://, data:, gopher://
},
// Timeouts and redirects control
timeout: { request: 5000 },
followRedirect: false, // or strict redirect allowlist
...options,
}).json()
}
}
export default new ReportHttp()
In addition to code controls, apply network-level mitigations such as egress filtering and deny-list updates to block access to known internal IP ranges from application containers. Combine JWT validation with these measures so that possessing a valid token does not extend the attack surface for SSRF.
Minimal JWT example with hostname checks
When decoding and using JWT claims, validate the token and then enforce destination rules before any outbound call.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { createRemoteJwtClient } from 'some-jwt-library'
export default class ProxyController {
public async proxyAsset({ request, response, auth }: HttpContextContract) {
const { assetId } = request.qs()
const user = await auth.authenticate()
// Map assetId to a known domain allowlist
const allowedDomains = ['assets.example.com', 'cdn.example.com']
const targetDomain = this.mapAssetToDomain(assetId)
if (!allowedDomains.includes(targetDomain)) {
return response.badRequest({ message: 'Asset domain not allowed' })
}
const client = createRemoteJwtClient({ baseUrl: `https://${targetDomain}` })
const data = await client.get(`/v1/assets/${assetId}`, {
headers: { Authorization: `Bearer ${user.token}` },
})
return response.ok(data)
}
private mapAssetToDomain(assetId: string): string {
// deterministic mapping, no user input in domain selection
return assetId.startsWith('img') ? 'assets.example.com' : 'cdn.example.com'
}
}