Clickjacking in Koa with Bearer Tokens
Clickjacking in Koa with Bearer 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 hidden or disguised element inside an embedded frame. Koa, a lightweight Node.js framework, does not set frame-protection headers by default. When a Koa application uses Bearer Tokens—typically passed via the Authorization header and often reflected in UI elements or embedded widgets—clickjacking can expose token leakage or unauthorized actions. For example, an attacker can craft a page that loads the Koa app in an invisible iframe and overlays interactive controls (like "Revoke Token" or "Transfer Funds") on top of legitimate buttons. If the user is authenticated with a Bearer Token and visits the malicious page, the attacker can hijack the user’s authenticated session without their knowledge.
Bearer Tokens amplify clickjacking risks in two ways. First, if the token is used for authorization only and the UI relies on the same origin for actions (e.g., POST /transfer), an embedded frame can trigger state-changing requests because cookies and Authorization headers are sent automatically by the browser. Second, if the Koa app embeds the token in JavaScript or HTML (e.g., for initializing an SDK or client-side rendering), an attacker who can read the frame via a clickjacking vector might harvest the token through UI manipulation or social engineering. Even with CORS in place, if X-Frame-Options or Content-Security-Policy frame-ancestors are missing or too permissive, the app becomes vulnerable.
Consider a Koa route that renders a page with an authenticated widget:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx) => {
const token = ctx.request.headers['authorization']?.split(' ')[1];
ctx.body = `
<div>
<h1>Widget</h1>
<button id="revoke">Revoke Token</button>
<script>
const token = "${token}"; // reflected token in page
document.getElementById('revoke').onclick = () => {
fetch('/api/revoke', { method: 'POST', headers: { Authorization: 'Bearer ' + token } });
};
</script>
</div>
`;
});
app.listen(3000);
If this page is loaded in an attacker-controlled site via an iframe, and the user’s browser sends the Authorization header automatically, a concealed button can trigger /api/revoke. The combination of Koa not enforcing frame restrictions and the presence of Bearer Tokens in the page creates an exploitable window. Attackers do not need to understand the internals of the token; they rely on the browser’s behavior to include credentials in cross-origin requests when the target route does not validate origin or use anti-CSRF protections.
Bearer Tokens-Specific Remediation in Koa — concrete code fixes
Remediation focuses on preventing embedding and enforcing strict frame policies, while safely handling Bearer Tokens in Koa. Below are concrete, copy-paste code examples.
1) Set frame protection headers. Use X-Frame-Options and Content-Security-Policy to restrict who can embed the app:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) =>
ctx.set('X-Frame-Options', 'DENY');
ctx.set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'"
);
await next();
});
app.use(async (ctx) => {
ctx.body = 'Secure Widget
Frame embedding is blocked.
';
});
app.listen(3000);
2) Avoid reflecting Bearer Tokens in HTML/JS. Never interpolate tokens into client-side scripts. Instead, keep token handling server-side and use opaque references or session identifiers where necessary:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
ctx.set('X-Frame-Options', 'DENY');
ctx.set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'"
);
await next();
});
app.use(async (ctx) => {
// Do NOT expose the token to the client.
// Use server-side sessions or secure storage instead.
const token = ctx.request.headers['authorization']?.split(' ')[1];
if (!token) {
ctx.status = 401;
ctx.body = { error: 'missing_token' };
return;
}
// Perform actions server-side; do not echo token to the page.
ctx.body = { status: 'ok', message: 'Widget rendered safely' };
});
app.listen(3000);
3) Enforce same-site and secure cookie attributes if using cookies alongside tokens. While Bearer Tokens are typically sent via headers, if your app also uses cookies for session management, set SameSite and Secure flags:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
ctx.set('X-Frame-Options', 'DENY');
ctx.set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'"
);
// If setting cookies, protect them
ctx.cookies.set('session', 'value', {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
await next();
});
app.use(async (ctx) => {
ctx.body = { status: 'secure' };
});
app.listen(3000);
4) Validate origins for sensitive actions. For endpoints that perform high-risk operations (e.g., token revocation), add origin or referer checks in addition to frame policies:
const Koa = require('koa');
const app = new Koa();
const allowedOrigin = 'https://your-app.com';
app.use(async (ctx, next) =>
if (ctx.path === '/api/revoke') {
const origin = ctx.request.get('Origin');
const referer = ctx.request.get('Referer');
if (!origin || origin !== allowedOrigin) {
ctx.status = 403;
ctx.body = { error: 'invalid_origin' };
return;
}
}
await next();
});
app.use(async (ctx, next) => {
ctx.set('X-Frame-Options', 'DENY');
ctx.set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'"
);
await next();
});
app.post('/api/revoke', async (ctx) => {
const token = ctx.request.headers['authorization']?.split(' ')[1];
if (!token) {
ctx.status = 401;
ctx.body = { error: 'missing_token' };
return;
}
// Proceed with revocation logic
ctx.body = { status: 'revoked' };
});
app.listen(3000);
These steps ensure that even if a Bearer Token is present, clickjacking cannot trigger unauthorized actions because the browser will refuse to render the app in frames and the server validates origins for sensitive endpoints.