Clickjacking: How Five Lines of CSS Can Steal a Click
The invisible iframe attack that tricks users into clicking something they never chose, and how to protect against it.
The invisible iframe attack that tricks users into clicking something they never chose, and how to protect against it.
You think you are accepting cookies. But you are actually starring a repo you’ve never heard of.
That’s clickjacking, a UI attack so simple it fits in a <style> block.
The principle is quite simple: it’s to trick the user into clicking something different from what they perceive they are clicking and thus diverting the click from its initial intent to trigger silently a sensitive action.
You are probably wondering why do victims fall for this? That’s because nowadays it is very common to have to accept cookies or dismiss a banner before accessing the page when visiting a website. Attackers only have to mimic those same intrusive elements.
Clickjacking or UI Redressing is a quite old attack with many countermeasures in place today. But I decided to write about it because I find it still interesting since it’s quite visual which changes from the usual misconfigured server attacks.
Also means of protection exist and are used but not everyone is really aware of them.
The attack has two layers. The attacker hosts a page the victim navigates to, for instance with a displayed cookie banner. On top of that page sits an invisible iframe loaded from the real target.
<style>
iframe {
position: absolute;
opacity: 0.001;
z-index: 10;
top: 0;
left: 0;
}
</style>
<!-- The lure the victim sees -->
<button>Accept cookies</button>
<!-- The real target, invisible -->
<iframe src="https://github.com/some-repo/star-toggle"></iframe>Three CSS properties do all the work:
| Property | Role |
|---|---|
opacity: 0.001 |
Makes the iframe invisible (!= 0 because some browsers would let click events pass through otherwise) |
z-index: 10 |
Places it on top so it receives the click |
position: absolute |
Aligns the iframe pixel-perfectly over the lure button |
The victim clicks “Accept cookies.” The browser dispatches that click to the iframe underneath. GitHub registers a star from an authenticated user. Send this lure to 10,000 developers and you’ve manufactured a trending repository out of nothing.
This is exactly what the victim sees: a single page with an “Accept all cookies” button. Drag the slider to rotate the scene and reveal what is really stacked on top.
A click is only half the attack. A smart attacker also needs the victim to not immediately realize something went wrong.
The solution is to listen for the click and swap the page content the moment it fires:
window.addEventListener('blur', () => {
// The iframe just stole focus: the victim clicked
showFakeConfirmation();
});When the victim clicks, the browser briefly shifts focus to the iframe. The attacker’s page detects that blur event and immediately replaces the lure with a natural looking screen. The victim closes the tab satisfied. The damage is already done.
The attack requires the victim to be logged into the target site. Here’s why that happens automatically, without the victim doing anything extra.
When you log into GitHub, the server issues a session cookie and the browser stores it. On every subsequent request to github.com, including requests made by an iframe on a completely different page, the browser attaches that cookie automatically. It has no way to ask “did the user intend this request?” It just sends the credential.
So when the attacker’s page loads github.com inside a hidden iframe, GitHub sees a valid authenticated session and renders your full account.
Browsers now default session cookies to SameSite=Lax, which tells them not to attach the cookie to cross-site subresource requests, including iframes. The target site loads inside the attacker’s frame as a logged-out page, which breaks the attack.
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnlyImpact scales with what the target site allows in a single click. Financial actions are the dramatic example, but the real breadth is wider:
The through-line: every server log, every audit trail looks completely clean. No exploit, no injection, no anomaly. A legitimate user clicked a legitimate button in a legitimate session.
Some real cases from the past two decades:
| Year | Target | What happened |
|---|---|---|
| 2008 | Adobe Flash Player | Mic and camera access granted via invisible Flash settings panel |
| 2009 | “Don’t Click” worm: forced tweets sent at scale from real accounts | |
| 2010 | Mass likejacking campaign, millions of involuntary likes | |
| 2015 | CSS-only invisible action buttons, no iframe needed | |
| 2018 | Yelp | Stored credit card charged via hijacked reservation confirm button |
An attack only lands if all three conditions are true simultaneously:
1. The target page can be embedded in an iframe. No defensive headers in place. (we will cover them shortly later) The browser loads the target inside any attacker-controlled frame without complaint.
2. A sensitive action fires on a single click. No re-authentication, no confirmation dialog, no CAPTCHA. The victim is already logged in and the action executes immediately on click.
3. The attacker can predict the button’s position. The target UI is public and stable. The attacker inspects it once, measures pixel offsets, then positions the iframe until the lure sits exactly over the target. This is not reliably preventable, which is why prerequisites 1 and 2 are the only real fixes.
As a website owner, you will need to configure the server to prevent the page from being embedded in an iframe. For this two headers are available. During the initial request to the page, the server will send a response header to the browser to indicate if the page can be embedded in an iframe.
The original fix, shipped in 2009 (MDN docs). It’s now legacy but it’s still useful.
# Block all framing
X-Frame-Options: DENY
# Allow framing from the same origin only
X-Frame-Options: SAMEORIGIN
# Allow framing from specific origins (does not work in Chrome or Safari)
X-Frame-Options: ALLOW-FROM https://trusted-partner.comDENY blocks all framing. SAMEORIGIN allows framing from the same origin only.
A third value, ALLOW-FROM, was supposed to allowlist specific origins, but it
was never implemented in Chrome or Safari and is effectively dead.
I will suppose you know what CSP is and how it works. If not, you can read the MDN documentation to get started.
The modern standard, strictly better than XFO is the frame-ancestors directive. It supports multiple allowed origins, wildcard subdomains, and a report-only mode for auditing before enforcing:
# Block all framing
Content-Security-Policy: frame-ancestors 'none';
# Allow same origin only
Content-Security-Policy: frame-ancestors 'self';
# Allowlist specific partners
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;When both headers are present and conflict, frame-ancestors takes precedence
in modern browsers.
Best practice: ship both. XFO covers older browsers; CSP covers everything else. The cost is two lines of server config.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';Before HTTP headers existed for this, developers wrote client-side scripts to detect iframe nesting and force a redirect:
if (window.top !== window.self) {
window.top.location = window.self.location;
}This was broken almost immediately after it appeared. The HTML5 sandbox
attribute lets an attacker disable top-level navigation on an iframe:
<iframe src="https://target.com" sandbox="allow-forms allow-scripts">
<!-- allow-top-navigation is absent: framebusting JS can't redirect -->
</iframe>Scan your app’s response headers tonight at securityheaders.com.
If X-Frame-Options or Content-Security-Policy with frame-ancestors is
missing, check your server configuration and add them before anything
else.
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
Set-Cookie: session=…; SameSite=Lax; Secure; HttpOnlyThe OWASP Clickjacking Defense Cheat Sheet is a good resource to dive a bit deeper into the topic.
That’s it for classic clickjacking. Headers kill it dead.
But it’s not over yet. New modern variants, that aren’t using iframes at all, are emerging.
For instance, DoubleClickjacking and DOM-based extension clickjacking bypass every defense covered here but the idea behind them is the same, divert the click from its initial intent to trigger a sensitive action.
I have given a talk about this topic so if you prefer to check the slides, here you can find
my deck.