Clickjacking in Adonisjs with Jwt Tokens
Clickjacking in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side vulnerability where an attacker tricks a user into interacting with a hidden UI element through an invisible or disguised iframe. When an application uses JWT tokens for authentication but relies solely on the server to determine authorization without embedding adequate UI-level protections, the combination can expose interactive features to clickjacking. In AdonisJS, APIs typically validate JWT tokens in middleware and then render views or serve endpoints that may include forms or action URLs. If these responses are served with an overly permissive Content-Security-Policy (CSP), or without anti-clickjacking headers, an authenticated session identified by a JWT can be embedded in a malicious site. For example, an attacker could craft a page that loads your AdonisJS dashboard route inside an iframe and overlay interactive controls, leveraging the user’s valid JWT-based session to perform actions such as changing settings or submitting forms without the user’s consent. Because JWTs are often stored in cookies or local storage and sent automatically by the browser, the server may treat the request as legitimate, even though the UI context is hostile. This is especially risky for state-changing POST endpoints that do not enforce same-origin policies or require explicit UI intent verification. The risk is not in JWT validation itself, but in how responses are framed and protected in the browser, which can allow an attacker to ‘click’ through your authenticated UI invisibly.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Defending against clickjacking in an AdonisJS application using JWT tokens involves a combination of HTTP headers, CSP directives, and careful UI design. Below are specific, actionable fixes with code examples.
1. Set anti-clickjacking HTTP headers
Ensure responses include headers that prevent the page from being embedded in iframes. In AdonisJS, you can add middleware to inject these headers globally or per route.
// start/kernel.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class HttpMiddleware {
public handle(ctx: HttpContextContract, next: () => Promise) {
// Prevent framing by any site
ctx.response.header('X-Frame-Options', 'DENY')
await next()
}
}
For more granular control, use CSP frame-ancestors instead of (or in addition to) X-Frame-Options, since X-Frame-Options is deprecated in some browsers.
// start/kernel.ts (continued)
public async handle(ctx: HttpContextContract, next: () => Promise) {
ctx.response.header(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'"
)
await next()
}
2. Protect JWT storage and transmission
JWTs should be transmitted over secure channels and stored in ways that reduce exposure to malicious UI overlays. Use httpOnly, Secure, and SameSite cookies for storing tokens rather than localStorage when possible, and enforce strict CORS policies.
// start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
Route.post('/login', async ({ request, response }: HttpContextContract) => {
const { email, password } = request.all()
// authenticate user ...
const token = generateJwt({ email })
response.cookie('auth_token', token, {
httpOnly: true,
secure: true, // only over HTTPS
sameSite: 'strict',
path: '/',
})
return response.json({ ok: true })
})
3. Require UI intent for sensitive actions
For critical operations (e.g., password change, fund transfer), require a user interaction that cannot be trivially automated through an iframe, such as a re-authentication step or a one-time token captured from a separate channel. Validate the origin header and consider embedding a per-request nonce in the page that must be submitted with the request.
// Example: validate Origin/Referer and a per-request CSRF-like nonce
// start/middleware/validate_ui_intent.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default async function validateUiIntent(ctx: HttpContextContract) {
const origin = ctx.request.header('origin')
const referer = ctx.request.header('referer')
if (!origin?.startsWith('https://your-trusted-domain.com')) {
ctx.response.status = 403
throw new Error('Invalid request origin')
}
// Expect a nonce embedded server-side in the page and sent in headers or body
const requestNonce = ctx.request.header('x-request-nonce')
const sessionNonce = ctx.session.get('page_nonce')
if (!requestNonce || requestNonce !== sessionNonce) {
ctx.response.status = 403
throw new Error('Missing or invalid UI intent nonce')
}
}
4. Combine with route-specific protections
For routes that render admin panels or sensitive forms, explicitly set CSP frame-ancestors and ensure tokens are not leaked in URLs or logs.
// Example route protection
Route.get('/admin/settings', async ({ view, response }) => {
response.header('Content-Security-Policy', "frame-ancestors 'none'")
return view.render('admin/settings', { token: getSessionToken() })
})