The Clickjacking Attack
Clickjacking tricks users into clicking something they didnβt intend to. An attacker overlays an invisible iframe on top of a decoy page β the user thinks theyβre clicking a button, but actually clicks something on the hidden page.
How It Works
Visible page:
βββββββββββββββββββββββββββ
β Click here to win! β
β βββββββββββββββββββββ β
β β [CLAIM PRIZE] β β β User thinks they click this
β βββββββββββββββββββββ β
βββββββββββββββββββββββββββ
Invisible iframe (opacity:0):
βββββββββββββββββββββββββββ
β β
β Twitter.com (hidden) β
β βββββββββββββββββββββ β
β β [TWEET] β β β Actually clicks this
β βββββββββββββββββββββ β
βββββββββββββββββββββββββββ
The Attack HTML
<style>
iframe {
width: 500px;
height: 500px;
position: absolute;
top: 0;
left: 0;
opacity: 0;
z-index: 100;
}
.decoy-button {
position: absolute;
top: 200px;
left: 100px;
z-index: 10;
}
</style>
<div class="decoy-button">Click me for a prize!</div>
<iframe src="https://victim-site.com/tweet"></iframe>
The user clicks the visible button, but actually clicks the invisible iframeβs βTweetβ button.
Defending Against Clickjacking
1. X-Frame-Options Header
The simplest defense. Send this HTTP header:
X-Frame-Options: DENY // never allow in iframe
X-Frame-Options: SAMEORIGIN // allow if same origin
Set it on the server:
// Express.js
res.setHeader('X-Frame-Options', 'DENY');
// Nginx
add_header X-Frame-Options "SAMEORIGIN" always;
// Apache
Header always set X-Frame-Options "DENY"
2. Content-Security-Policy frame-ancestors
The modern approach β more flexible:
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors https://trusted-site.com;
3. Frame Busting with JavaScript
A client-side defense (works if JS is enabled):
if (window.top !== window.self) {
window.top.location = window.self.location;
}
But this can be bypassed. The sandbox attribute on the attackerβs iframe can prevent it:
<iframe sandbox="allow-scripts" src="..."></iframe>
Without allow-top-navigation, the frame-busting JS canβt redirect the top window.
4. The Cookie-Based Double Submit Pattern
Generate a random token, store it in a cookie and session, and verify on sensitive actions:
// On sensitive form submission
if (req.cookies.csrf_token !== req.session.csrf_token) {
// reject β likely clickjacking or CSRF
}
Detecting Iframe Embedding
<div id='frame-detect' style='padding:16px;background:#f1f5f9;border-radius:8px;'>
<p id='detect-status'>Checking if we're in an iframe...</p>
<pre id='detect-log' style='background:white;padding:8px;border-radius:4px;'></pre>
</div> const log = document.getElementById('detect-log');
const status = document.getElementById('detect-status');
try {
if (window.top !== window.self) {
status.textContent = 'β οΈ I am embedded in an iframe!';
status.style.color = '#ef4444';
log.textContent =
'window.self: ' + window.self.location.href + '\\n' +
'window.top: ' + window.top.location.href + '\\n' +
'These are DIFFERENT β we are framed!';
} else {
status.textContent = 'β
I am the top-level window. Not framed.';
status.style.color = '#22c55e';
log.textContent =
'window.self === window.top: true' + '\\n' +
'This page is not embedded in any iframe.';
}
} catch (e) {
// Cross-origin access to top.location throws
status.textContent = 'β οΈ Cannot access top β likely cross-origin iframe';
status.style.color = '#eab308';
log.textContent = 'SecurityError: ' + e.message + '\\n' + 'This means we\\'re likely in a cross-origin iframe.';
} window.self vs window.top
| Property | What it is |
|---|---|
window.self | The current window/iframe |
window.top | The topmost browser window |
window.parent | The direct parent (might be an iframe too) |
When window.self !== window.top, youβre inside an iframe.
Best Practices
- Always set
X-Frame-Options: DENYon auth pages (login, payment) - Use
Content-Security-Policy: frame-ancestorsfor fine-grained control - Donβt rely solely on JavaScript frame busting β headers are the primary defense
- For pages that legitimately need framing (widgets, embeds), restrict to specific origins:
Content-Security-Policy: frame-ancestors https://partner.com; - Consider adding
SameSite=Strictcookies for sensitive operations
Key Takeaways
- Clickjacking hides a victim site in an invisible iframe over a decoy page
X-Frame-Options: DENYis the simplest server-side defenseCSP frame-ancestorsis the modern, more flexible approach- Frame-busting JS (
if (top !== self)) is a secondary defense - Always protect login, payment, and social-action pages from framing
- The
sandboxattribute can be used to restrict what framed content can do