The Clickjacking Attack
Understanding clickjacking attacks and how to defend against them
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
Keep learning
Related tutorials from across the library.
Popups and Window Methods
Open, control, and communicate with browser popup windows
Web Development / Frontend / JavaScript DOM / Frames-Windows
Cross-Window Communication
Communicate between browser windows, tabs, and iframes with postMessage
Web Development / Frontend / JavaScript DOM / Frames-Windows
Browser Environment & Specs
Understand the JavaScript browser environment - window, DOM, BOM, and ECMAScript
Web Development / Frontend / JavaScript DOM / Document