Clickjacking is a deceptively simple attack with significant consequences. An attacker embeds your website in an invisible iframe on their own page, then positions it over a decoy button or link. When a user thinks they are clicking something harmless — a "claim your prize" button, a video play icon — they are actually clicking a button on your invisible website. The click is real. The consent is not.
How Clickjacking Works
The attack relies on the browser's ability to embed any web page inside an iframe. The attacker's page positions the iframe with opacity: 0 or uses careful z-index layering to overlay it on their content:
<!-- Attacker's page -->
<style>
iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
opacity: 0; /* Invisible */
z-index: 2; /* On top of everything */
pointer-events: all;
}
.decoy-button {
position: absolute;
top: 200px; left: 300px; /* Aligned with "Delete Account" button in the iframe */
z-index: 1;
}
</style>
<button class="decoy-button">Click here to win a prize!</button>
<iframe src="https://your-bank.com/transfer?to=attacker&amount=1000"></iframe>
The victim sees the "Click here to win a prize!" button. They click it. The click registers on the invisible bank transfer form positioned behind it. If the victim was authenticated to their bank — which is likely, since browsers send cookies automatically — the transfer executes.
Real-World Targets
Clickjacking is most dangerous on pages that perform significant actions with a single click:
- Bank transfer confirmation pages
- OAuth authorization flows ("Allow this app to access your account")
- "Delete account" or "Delete all data" buttons
- Social media "Like" or "Follow" buttons (Facebook had a major clickjacking vulnerability in 2009)
- Admin panel action buttons
- One-click purchase flows
Prevention: X-Frame-Options
The simplest prevention is a single HTTP response header:
X-Frame-Options: DENY
This instructs the browser to refuse to render the page inside any iframe — regardless of which site is doing the embedding. Use DENY unless you have a legitimate reason to allow your page to be framed. If you need same-origin framing (e.g., an app that uses iframes internally), use SAMEORIGIN.
X-Frame-Options: SAMEORIGIN
There is no longer a supported ALLOW-FROM value — browser support was dropped. If you need to allow specific third-party origins to frame your content, use CSP's frame-ancestors instead.
Prevention: Content Security Policy frame-ancestors
The modern approach uses CSP's frame-ancestors directive, which is more flexible and overrides X-Frame-Options in browsers that support CSP:
<!-- Block all framing -->
Content-Security-Policy: frame-ancestors 'none';
<!-- Allow same origin only -->
Content-Security-Policy: frame-ancestors 'self';
<!-- Allow specific trusted third-party -->
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;
Use both headers for maximum browser compatibility:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
Implementation Examples
Nginx:
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
Apache:
Header always set X-Frame-Options "DENY"
Header always set Content-Security-Policy "frame-ancestors 'none'"
Flask:
@app.after_request
def anti_clickjack(response):
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
return response
Frame-Busting Scripts: Why They Fail
Before these headers existed, developers used JavaScript "frame-busting" scripts:
if (top !== self) { top.location = self.location; }
These are unreliable and should not be used as a primary defense. Attackers can neutralize them by adding sandbox="allow-forms allow-scripts" to the iframe element, which prevents the framed page from navigating the top frame. The HTTP header approach is immune to this bypass because it is enforced by the browser before the page loads, not by code that can be sandboxed away.
Does Your Site Need This?
Virtually every web application should set X-Frame-Options: DENY or frame-ancestors 'none'. The only legitimate exception is if your application is intentionally designed to be embedded in iframes — a widget, an embeddable payment form, or a dashboard component meant to be framed by partner sites.
If you are not sure whether your site has this header set, check with:
curl -sI https://yoursite.com | grep -iE "x-frame|frame-ancestors"
If the output is empty, your site is currently frameable by any website on the internet.