The clickjacking attack
Clickjacking is a trick where a malicious page gets a visitor to click something on a different site without realizing it. The click happens on the victim site, but it’s carried out with the visitor’s own logged-in session, so the target treats it as a genuine, intentional action.
This isn’t theoretical. Twitter, Facebook, PayPal, and plenty of other big names shipped this vulnerability at some point. They’ve since fixed it, but the technique is worth understanding because the same mistake keeps reappearing in new UIs.
The idea
The mechanism is short enough to state in one breath: put the victim’s button where the user thinks something harmless is, make it invisible, and let the user click.
Here’s the classic Facebook version, step by step:
- The visitor lands on the attacker’s page. How they got there doesn’t matter — a link in a DM, a spammy tweet, a search result.
- That page shows a friendly-looking lure: a link or button that says something like “you won a free gift” or “tap to see the surprise”.
- On top of the lure, the attacker floats a transparent
<iframe>whosesrcpoints at facebook.com, aligned so the Facebook “Like” button sits exactly over the lure.z-indexputs the iframe on top. - The visitor aims for the lure and clicks. Because the invisible iframe is on top, the click lands on the Like button instead.
The demo
Here’s what the attacker’s page looks like. To make the trick visible for teaching, the <iframe> is set to half-transparent. On a real attack page it would be fully transparent.
<style>
iframe { /* iframe from the victim site */
width: 400px;
height: 100px;
position: absolute;
top:0; left:-20px;
opacity: 0.5; /* real attacks use opacity:0 */
z-index: 1;
}
</style>
<div>You won a free gift! Tap to open:</div>
<!-- The url from the victim site -->
<iframe src="/clickjacking/victim.html"></iframe>
<button>Open my gift</button>
<div>...Enjoy! (just kidding — that click was mine)</div>
The full demo of the attack — the victim’s Like button is floated over the lure at 50% opacity so you can see the overlap. Click Open my gift and watch which button actually receives the click:
In this version the half-transparent <iframe src="facebook.html"> is visible, floating over the button. When you click the button, the click really goes to the iframe. On a real page you wouldn’t see any of this — the iframe is fully transparent, so all you perceive is the button underneath.
If the visitor is already signed in to Facebook (and “remember me” keeps most people signed in), the stolen click registers a “Like”. On Twitter the same trick lands a “Follow”; on a bank it might confirm a transfer.
Here’s the same setup, but closer to a real attack, with opacity:0 — the overlay is completely invisible. Tick reveal the hidden layer to see what’s really sitting under your cursor, then click Open my gift:
The whole attack reduces to one alignment problem: position the <iframe> so the target control sits exactly over the lure. Click the lure, hit the button. CSS makes that easy.
Old-school defenses (weak)
The oldest defense is a scrap of JavaScript that refuses to run inside a frame. It’s called “framebusting”.
if (top != window) {
top.location = window.location;
}
The logic: if this window notices it isn’t the top-level window, it forces itself to become the top by navigating the outer page to its own URL. The frame “busts” out of the frame.
The very first step is that check — a page working out whether it’s being framed. This Playground itself runs inside an iframe, so the check below really does report “framed”. Click it and see:
It reads like a clean fix. It isn’t. There are several ways to defeat it, and here are a few.
Blocking top-navigation
The attacker can stop the top.location change from ever completing by hooking the beforeunload event on the outer page they control.
window.onbeforeunload = function() {
return false;
};
When the framed page tries to reassign top.location, the browser is about to navigate the top window away — which triggers beforeunload, and the visitor gets a “Do you want to leave this page?” prompt.
The visitor has no idea a hidden iframe is trying to escape. All they see is the attacker’s page, and they have no reason to leave it, so they click “Stay”. The navigation is canceled and top.location never changes. Framebusting defeated.
Play the whole exchange out. Leave the guard on, hit Framebust, then click Stay the way a real visitor would — and watch the escape attempt die:
Sandbox attribute
The sandbox attribute on an iframe locks down what the framed page is allowed to do, and one of the things it blocks is navigation. A sandboxed iframe can’t reassign top.location, which means it can’t framebust.
The attacker keeps the parts they need and drops the part that hurts them. Adding allow-scripts and allow-forms relaxes the sandbox enough to run the victim’s scripts and submit its forms. Leaving out allow-top-navigation keeps the top-navigation ban in force.
<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>
X-Frame-Options
The real fix is a server-side HTTP response header: X-Frame-Options. It tells the browser whether this page is allowed to be shown inside a frame at all. The browser enforces it — no JavaScript on the page can be tricked or disabled.
One rule that trips people up: it only counts as an HTTP header. The browser ignores it inside an HTML <meta> tag, so <meta http-equiv="X-Frame-Options" ...> does nothing. It has to come down in the server’s response headers.
The header takes one of three values:
For example, Twitter uses X-Frame-Options: SAMEORIGIN.
Here’s the result of trying to frame it:
<iframe src="https://twitter.com"></iframe>
Depending on your browser, the iframe above is either blank or shows a message telling you the browser refused to display that page in a frame.
Showing with disabled functionality
X-Frame-Options has a blunt side effect: once you send it, nobody can frame your page — including partners and embedders who have a legitimate reason to. If you want to be framed sometimes but stay safe, you need a softer approach.
One option is to cover the page with a full-size <div> that swallows every click. That overlay is removed only when the page confirms it’s running as the top window (or otherwise decides protection isn’t needed).
<style>
#protector {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 99999999;
}
</style>
<div id="protector">
<a href="/" target="_blank">Go to the site</a>
</div>
<script>
// reading top.document throws if the top window is a different origin,
// but that's fine here — an error just means we stay protected
if (top.document.domain == document.domain) {
protector.remove();
}
</script>
The idea: if the page is framed by a cross-origin attacker, top.document.domain throws a security error, the remove() line never runs, and the protector stays up blocking clicks. If the page is on top (or framed by your own same-origin page), the check passes and the overlay disappears so the page works normally.
Toggle whether the page is framed by a cross-origin attacker, then try to press Confirm purchase. When the protector is up, your click can’t get through:
SameSite cookie attribute
Cookies give you another angle. The samesite cookie attribute can block clickjacking too, and it attacks the problem at the root — the visitor’s authenticated session.
A cookie marked samesite is only attached to a request when the site is opened directly, as the top-level page. Load that site inside a frame belonging to another origin, and the browser leaves the cookie out.
More on the attribute in Cookies, document.cookie.
Say Facebook put samesite on its authentication cookie:
Set-Cookie: authorization=secret; samesite
Now when the attacker frames Facebook from their own domain, the browser doesn’t send that cookie. Facebook sees an anonymous, logged-out visitor inside the frame — so even a stolen click can’t “Like” anything on behalf of a real account. The attack collapses.
The catch: samesite only helps when the sensitive action depends on cookies. If a page doesn’t authenticate with cookies, the attribute does nothing to protect it, which also means such public pages can still be framed freely by other sites.
Summary
Clickjacking tricks users into clicking on a victim site without knowing it’s happening. It’s dangerous exactly when a single click triggers something important — a purchase, a follow, a permission grant, a money transfer.
An attacker just needs to get you onto their page: a link in a message, a lure in a feed, any of a hundred variations. From there they steal one click.
What makes it sneaky is a blind spot in how we design UIs. We assume a click means the user meant to click here, now, on this. We rarely design for an adversary clicking on the visitor’s behalf, so the holes show up in places nobody thought to guard.
The defenses, from strongest to most flexible:
- Send
X-Frame-Options: SAMEORIGIN(or useContent-Security-Policy: frame-ancestors) on any page — or the whole site — that has no business being embedded in someone else’s frame. - If you do want to allow framing but still stay safe, use a covering
<div>that only lifts when a same-origin check confirms it’s safe. - Mark authentication cookies
samesiteso a framed cross-origin copy of your site is treated as logged out.