Clickjacking in Hapi with Jwt Tokens
Clickjacking in Hapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side UI redress attack where an attacker tricks a user into interacting with a transparent or opaque element over a legitimate page. In a Hapi application that uses JWT tokens for authentication, the presence of JWTs does not inherently protect against clickjacking; if the application delivers authenticated pages without anti-clickjacking defenses, a malicious site can embed the app’s routes in an iframe and capture user actions.
When JWT tokens are stored in cookies (rather than being used only as bearer tokens in Authorization headers), and the app relies on the cookie’s automatic inclusion by the browser, an embedded iframe can make authenticated requests on behalf of the victim. Even if routes require a JWT in the Authorization header, developer mistakes—such as exposing an endpoint that returns HTML with an authenticated session and lacks frame-protection headers—can create a usable attack surface. For example, an app might have a route /profile that returns user data and a cookie-based JWT; if this route is rendered in a page without X-Frame-Options or Content-Security-Policy, an attacker’s page can load it invisibly and overlay interactive elements to hijack actions like updating email or changing settings.
Hapi’s security posture in this context depends on the developer explicitly setting headers and designing the authentication flow to avoid automatic credentialing in iframes. JWT tokens in Authorization headers are less exposed to clickjacking because the browser does not include them automatically in cross-origin requests; however, if the app also exposes cookie-based session tokens or omits frame-protection headers, the combination of Hapi, browser behavior, and JWT usage can inadvertently enable clickjacking. Real-world patterns include pages that embed third-party widgets or developer consoles that fail to set X-Frame-Options or Content-Security-Policy: frame-ancestors, allowing an attacker’s site to load the authenticated UI and overlay phishing controls.
To illustrate a vulnerable Hapi setup, consider a server that sets a JWT in an HTTP-only cookie and does not set frame-protection headers:
const Hapi = require('@hapi/hapi');
const cookie = require('@hapi/cookie');
const jwt = require('jsonwebtoken');
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
await server.register(cookie);
server.auth.scheme('jwtScheme', (server) => {
return {
authenticate(request, h) {
const token = request.state.auth;
if (!token) { return h.authenticated({ credentials: null }); }
try {
const decoded = jwt.verify(token, 'weak-secret');
return h.authenticated({ credentials: decoded });
} catch (err) {
return h.authenticated({ credentials: null, mode: 'no-cookie' });
}
}
};
});
server.auth.strategy('jwt', 'jwtScheme');
server.auth.default('jwt');
server.route({
method: 'GET',
path: '/profile',
options: {
auth: 'jwt',
handler: (request, h) => {
return `Profile
Email: ${request.auth.credentials.email}
`;
}
}
});
await server.start();
};
init();
In this example, the JWT is stored in a cookie (auth), and the route returns HTML that could be embedded. Without X-Frame-Options or a strict CSP frame-ancestors, an attacker can load https://localhost:4000/profile in an iframe and overlay controls to trigger state-changing requests if the user is authenticated.
To mitigate, Hapi applications should set frame-protection headers and avoid relying on cookies for privileged actions when embedding is possible. Defense-in-depth includes using Content-Security-Policy: frame-ancestors 'none' and ensuring that JWT usage in Authorization headers is paired with these headers so that even if a page is embedded, the browser will not send credentials to the attacker’s context.
Jwt Tokens-Specific Remediation in Hapi — concrete code fixes
Remediation focuses on two areas: preventing the browser from embedding authenticated pages and ensuring JWT usage does not inadvertently enable cross-origin credential inclusion. For Hapi, set security headers on every response and prefer JWT transmission via the Authorization header instead of cookies where feasible.
1) Set X-Frame-Options and Content-Security-Policy on all responses. In Hapi, you can use a server extension or a simple route pre-handler to inject headers:
server.ext('onPreResponse', (request, h) => {
const response = request.response;
if (response.variety === 'view') {
response.header('X-Frame-Options', 'DENY');
response.header('Content-Security-Policy', "frame-ancestors 'none'");
}
return h.continue;
});
2) Avoid storing JWTs in cookies for routes that render HTML. Instead, require the client to send the JWT in the Authorization header and configure Hapi authentication to reject cookie-only credentials for sensitive routes:
server.auth.strategy('jwtAuth', 'jwt', {
key: process.env.JWT_SECRET,
validate: (decoded) => ({ isValid: true }),
verifyOptions: { algorithms: ['HS256'] }
});
server.route({
method: 'GET',
path: '/profile',
options: {
auth: {
strategies: ['jwtAuth'],
mode: 'required'
},
handler: (request, h) => {
const user = request.auth.credentials;
return { email: user.email };
}
}
});
3) If you must use cookies for convenience, scope them appropriately and ensure the SameSite attribute is set to Strict or Lax and the Secure flag is used in production:
server.state('auth', {
ttl: 2 * 60 * 60 * 1000,
isSecure: true,
isHttpOnly: true,
encoding: 'base64json',
strictHeader: true,
clearInvalid: true,
rejectUnauthorized: true,
sameSite: 'strict'
});
4) For SPAs, consider a double-submit cookie pattern or use the Authorization header exclusively; never rely on cookies for privilege decisions without CSP frame-ancestors and X-Frame-Options. Combine these headers with runtime scanning (e.g., middleBrick scan) to detect missing protections and validate that your CSP frame-ancestors directive is not overly permissive.
These fixes reduce the risk of clickjacking against JWT-authenticated Hapi endpoints by ensuring that browsers do not embed authenticated content and that tokens are not automatically included in cross-origin contexts.