What Clickjacking Is
Clickjacking (also called UI redressing) is an attack where a malicious web page embeds your application in a transparent or invisible iframe. Decoy content is layered underneath β a button, a game, a "click here to win" prompt. When the user clicks what they think is the attacker's UI, they're actually clicking on a button in your application.
The attack doesn't require any vulnerability in your frontend code. It exploits the browser's ability to embed any URL in an iframe. If your server allows being framed, an attacker can wrap your app in their page.
Why it's still relevant: Clickjacking is simple to prevent but frequently overlooked. In penetration tests, missing X-Frame-Options is one of the most consistent findings across web applications. It takes minutes to fix; it often just never gets done.
How the Attack Works Step by Step
<!-- Attacker's page β user sees "Win a prize!" button --> <style> .decoy { position: absolute; top: 200px; left: 300px; z-index: 2; } iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0.0001; /* transparent, but still interactive */ z-index: 3; pointer-events: all; } </style> <div class="decoy"><button>Click to claim your prize!</button></div> <iframe src="https://victim-bank.com/transfer"></iframe> <!-- Iframe is positioned so "Confirm Transfer" aligns with the decoy button --> <!-- User clicks decoy β actually clicks Confirm Transfer in the bank's UI -->
The iframe is made nearly transparent with opacity: 0.0001 (not 0 β 0 disables pointer events in some browsers). The attacker positions the iframe so a dangerous button in your app aligns with their decoy click target. The user sees the attacker's page and clicks the attacker's element β but the click registers on your app's button inside the invisible iframe.
Real-World Clickjacking Examples
Clickjacking has been used in practice to:
- Transfer funds β banking apps without frame protection are vulnerable to users being tricked into confirming transfers they never initiated
- Change account settings β email address, password, security questions β by overlaying the settings page
- Enable permissions β camera/microphone access in Flash (pre-HTML5) was a major vector, with users clicking "allow" without realising it
- Delete or modify data β administrative delete buttons are high-value targets
- Follow/like/share β social media interactions driven by clickjacking (see "Likejacking" below)
Likejacking and Social Media Exploits
Likejacking was a widespread early clickjacking variant where Facebook's "Like" button was embedded invisibly over enticing content. Users thought they were clicking a cat video thumbnail and instead liked a page, which then appeared in their feed and spread the attack virally. At peak, these campaigns reached millions of users within hours of deployment.
The same pattern works for Twitter/X follows, YouTube subscriptions, or Google reviews. Any single-click action in an iframe can be weaponised this way.
X-Frame-Options Header
The X-Frame-Options HTTP response header tells browsers whether a page can be embedded in a frame, iframe, or object. Three values:
DENYβ never allow framing, by anyoneSAMEORIGINβ only allow framing by pages on the same originALLOW-FROM https://trusted.example.comβ deprecated, not supported in modern browsers
from flask import Flask app = Flask(__name__) @app.after_request def set_security_headers(response): response.headers["X-Frame-Options"] = "DENY" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" return response
const helmet = require('helmet'); // helmet sets X-Frame-Options: SAMEORIGIN by default app.use(helmet()); // Or explicitly: app.use(helmet.frameguard({ action: 'deny' }));
Content Security Policy frame-ancestors
The CSP frame-ancestors directive is the modern replacement for X-Frame-Options. It's more flexible, better supported in modern browsers, and part of the broader CSP framework.
# Deny all framing add_header Content-Security-Policy "frame-ancestors 'none'" always; # Allow framing only from same origin add_header Content-Security-Policy "frame-ancestors 'self'" always; # Allow specific trusted origins (useful for embeds) add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-partner.com" always;
frame-ancestors 'none' is equivalent to X-Frame-Options: DENY. frame-ancestors 'self' is equivalent to SAMEORIGIN. Unlike X-Frame-Options, CSP frame-ancestors supports multiple origins and wildcards.
Which Is Better: X-Frame-Options vs CSP frame-ancestors?
For most applications: set both. X-Frame-Options covers older browsers; frame-ancestors in CSP handles modern ones and offers more granular control. When both are present, frame-ancestors takes precedence in supporting browsers.
# Belt and braces β set both add_header X-Frame-Options "DENY" always; add_header Content-Security-Policy "frame-ancestors 'none'; default-src 'self'" always;
One of the fastest security wins available: Adding these two headers takes less time than reading this article. They're pure server configuration β no code changes, no testing required, no breaking changes for users who aren't embedding your app in iframes.
When You Legitimately Need Iframes
Some use cases genuinely need to be embeddable: payment widgets embedded in merchant sites, video players, map embeds, customer support widgets. For these, use frame-ancestors with an explicit allowlist of permitted origins β not a broad allow-all. Avoid wildcards for sensitive pages; allow specific origins only for the specific pages that need embedding.
JavaScript Frame-Busting (and Why It Doesn't Work)
Before X-Frame-Options existed, developers used JavaScript to detect and break out of frames:
// Classic frame-busting script β DO NOT rely on this if (window.top !== window.self) { window.top.location = window.self.location; } // Bypassed easily by attacker using sandbox attribute: // <iframe src="victim.com" sandbox="allow-forms allow-scripts"></iframe> // sandbox without allow-top-navigation prevents frame-busting code from working
Frame-busting via JavaScript is bypassable using the iframe sandbox attribute without allow-top-navigation. JavaScript frame-busting is not a reliable defence. Use HTTP headers β they're enforced by the browser before JavaScript executes.
Prevention Checklist
- Add X-Frame-Options: DENY β to all pages that don't need to be embeddable
- Add CSP frame-ancestors 'none' β or
'self'if same-origin framing is needed - Set headers at the server/proxy level β Nginx, Apache, or CDN β not just in application code
- For embeddable widgets, use an explicit allowlist β
frame-ancestors 'self' https://partner.com - Never rely on JavaScript frame-busting β it's bypassable
- Test with security header scanners β securityheaders.com, OWASP ZAP headers scan
- Include in CI checks β AquilaX DAST checks for missing framing headers in every scan
Check Your Security Headers Automatically
AquilaX DAST scans your live application for missing security headers β X-Frame-Options, CSP, HSTS, and more β as part of every automated security scan.
Start Free Scan