Clickjacking embeds your target site inside an invisible iframe, overlays it on a decoy page, and tricks authenticated users into clicking buttons they can't see - transferring funds, granting OAuth permissions, posting to social feeds, or enabling webcam access. The fix is one HTTP response header: Content-Security-Policy: frame-ancestors 'none' (or 'self' if you need legitimate embedding). X-Frame-Options is the legacy equivalent. JavaScript frame busting and SameSite cookies add defence-in-depth but are not standalone fixes. Every web application should have this header. Most don't.
// 01 What is clickjacking?
Clickjacking - formally called UI redressing - is an interface-based client-side attack in which the victim is deceived into clicking on content belonging to a hidden website while believing they are interacting with a visible decoy page. The technique requires no vulnerability in the victim's browser beyond its standard, intentional ability to render iframes. The attacker constructs a page, hosts it anywhere, and tricks the victim into visiting it - typically via phishing, malicious advertising, or a compromised third-party widget.
The core mechanic is simple: load the target application inside an iframe, make the iframe transparent, position a visible decoy element underneath the invisible target button, and wait for the authenticated user to click the decoy. Their click lands on the hidden target application, performing whatever action the attacker positioned beneath the cursor. Because the victim is already authenticated to the target site - their session cookie is sent automatically - the action executes with their full privileges.
Clickjacking is listed in the OWASP Top 10 under A05: Security Misconfiguration and was a prominent entry in earlier editions specifically. It appears routinely in web application penetration tests because the fix (adding an HTTP header) is trivial and the oversight (omitting it) is extremely common.
// 02 How a clickjacking attack is constructed
Clickjacking attacks are built using CSS to create precisely positioned, transparent layers. The attacker loads the target website as an iframe - positioned to fill the browser viewport - and sets its opacity to zero so it is completely invisible. A decoy page with enticing visible content sits beneath the iframe in the stacking order (z-index), at the exact pixel offset required to align a target button with whatever visible element the attacker wants the user to click.
HTML + CSS<!-- Basic clickjacking page structure -->
<html>
<head>
<style>
#target_website {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
border: none;
opacity: 0.0; /* invisible to the victim */
z-index: 2; /* sits on top - receives all clicks */
}
#decoy_website {
position: absolute;
z-index: 1; /* visible decoy content sits below */
}
</style>
</head>
<body>
<div id="decoy_website">
<h1>You just won a $1,000 prize!</h1>
<button>Click here to claim</button> <!-- visible bait -->
</div>
<iframe id="target_website"
src="https://bank.example.com/transfer?amount=5000&to=attacker">
</iframe> <!-- invisible - click lands here -->
</body>
</html>
/* The victim sees the prize button.
They click it. Their click hits the bank transfer confirm button
in the invisible iframe. The transfer executes with their session. */
Position values are tuned with absolute and relative offsets so the target action overlaps the decoy precisely regardless of screen resolution. Attackers often use a near-zero opacity (e.g., 0.001) rather than 0.0 to evade threshold-based transparency detection present in some browsers - a sufficient effect without triggering heuristics that block fully transparent iframes.
"The attack doesn't exploit a bug in the browser. It exploits the browser doing exactly what it was designed to do: render an iframe and process a click. That's what makes it persistent - there's no vendor patch coming."
CyberFortify Web Application Security Practice// 03 Clickjacking attack variants in the wild
The basic clickjacking template spawns several distinct attack variants depending on what the attacker wants to achieve. Understanding these variants is essential for comprehensive testing - a generic iframe check catches the basic case but misses cursor hijacking and drag-and-drop attacks entirely.
Likejacking
The original mass-scale clickjacking variant. An invisible Facebook, LinkedIn, or Twitter Like/Follow button is overlaid on compelling content - news articles, viral videos, prize pages. Victims click the decoy and unknowingly endorse attacker-controlled content to their entire social network, amplifying phishing or disinformation campaigns.
Multi-step clickjacking
Sequences multiple decoy interactions across a workflow that requires several user clicks to complete - such as OAuth application authorization dialogs or two-step wire transfer confirmations. Each visible decoy click advances the hidden application through one step of its confirmation flow.
Drag-and-drop hijacking
Exploits the HTML5 drag-and-drop API to exfiltrate data. The attacker positions a hidden input field from the target application under a visible drag target. When the user drags an attractive element (e.g., a "move this file" widget), the drag action deposits content - including clipboard data or file paths - directly into the hidden form field.
Cursor hijacking
Uses CSS cursor: url() to replace the system cursor with a custom image offset by a fixed number of pixels. The victim perceives their cursor to be at one position while actual click events register elsewhere - effectively applying a persistent spatial offset to every click on the page without requiring an iframe layer.
File upload hijacking
Positions a transparent file input element from a target site over an innocuous-looking "upload your avatar" widget. The victim selects and submits a file through what appears to be a normal upload dialog; instead, the file is uploaded to the attacker's target site under the victim's authenticated session.
Permission hijacking
Overlays browser permission grant dialogs - camera, microphone, geolocation, notification - on decoy pages. Targets users into granting device permissions to attacker-controlled origins while believing they are approving something unrelated. Particularly effective on mobile browsers where permission dialogs occupy a smaller portion of the viewport.
// 04 What an attacker can actually achieve
The impact of a successful clickjacking attack is bounded by what authenticated actions the target application exposes to a single click. In practice, that scope is broader than most developers assume:
- Financial transfers - any banking or payment application with a confirm-transfer button that can be pre-populated via URL parameters is trivially exploitable. The attacker pre-fills the destination account and amount via query string; the victim clicks "confirm".
- Account takeover prerequisites - clickjacking the "add secondary email" or "change password" form primes an account for subsequent takeover without requiring credentials. The attacker adds their own recovery email before the victim realizes anything happened.
- OAuth scope expansion - social-login and OAuth authorization flows often present a single "Allow" button. Clickjacking that button grants the attacker's registered application elevated scopes, including read access to contacts, calendar data, or drive files.
- Privilege escalation in admin panels - a low-privilege authenticated user can be manipulated into clicking a hidden "grant admin" or "disable MFA" button in an admin panel they have read-only access to, if framing protections are absent on the admin domain.
- CSRF amplification - where CSRF tokens would normally block cross-site form submission, clickjacking bypasses the token requirement entirely because the submission originates from the victim's own browser session, not a cross-site request.
// 05 How to test for clickjacking vulnerabilities
During a web application penetration test, clickjacking is assessed on every sensitive endpoint - not just the homepage. A page that lacks framing controls only on its /account/delete endpoint is just as exploitable as one that lacks them globally. The test methodology is:
Check response headers on each sensitive endpoint
For every endpoint that performs a state-changing action (transfers, deletions, permission changes, profile updates), inspect the HTTP response headers for X-Frame-Options and Content-Security-Policy: frame-ancestors. Missing on any one endpoint is a finding - even if all others are protected.
Attempt iframe embedding
Create a local HTML file that loads the target URL inside an iframe. Open it in a browser. If the page renders inside the iframe, framing protection is absent. Use browser DevTools to inspect whether any Content-Security-Policy violations appear in the console - their presence confirms a header exists but may indicate misconfiguration.
Test the overlap scenario
For a confirmed finding, build a proof-of-concept that demonstrates impact: position the iframe so a specific target button (e.g., "Confirm Transfer", "Delete Account") overlaps a visible decoy button. Screenshot the overlay. This is what goes into the report - a theoretical finding needs a working PoC to be actionable.
Check frame-busting bypasses
If JavaScript frame busting is the only protection, test whether it can be bypassed via the sandbox attribute on the iframe element: <iframe sandbox="allow-forms allow-scripts" ...>. The sandbox attribute prevents the framed page's JavaScript from executing, neutralising frame busting without affecting the iframe render.
Assess severity by action
Rate severity based on what the clickjackable action does. Account deletion or financial transfer → Critical. Privilege escalation → Critical. Profile update or social share → Medium. Static read-only page → Informational (framing a page that performs no state-changing action has no direct impact).
// 06 Prevention: X-Frame-Options header
The X-Frame-Options response header is the original server-side mechanism for preventing clickjacking. It instructs the browser not to render the response inside a <frame>, <iframe>, <embed>, or <object> element. It was introduced by Internet Explorer 8 in 2009 and is now universally supported across modern browsers.
HTTP HEADERS# DENY - page cannot be embedded in any iframe, anywhere
X-Frame-Options: DENY
# SAMEORIGIN - page can only be embedded by pages on the same origin
X-Frame-Options: SAMEORIGIN
# ALLOW-FROM (deprecated - not supported in Chrome or Firefox)
# Do not use - falls back to no protection in modern browsers
X-Frame-Options: ALLOW-FROM https://trusted-partner.com
Set this header at the web server or reverse proxy level so it applies to all responses, not just those generated by application code. Missing it on even one route can be exploited.
SERVER CONFIG# Nginx - add to server {} or location {} block
add_header X-Frame-Options "DENY" always;
# Apache - add to httpd.conf or .htaccess
Header always set X-Frame-Options "DENY"
# Express.js (Node) - use helmet middleware
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
# Django - set in settings.py
X_FRAME_OPTIONS = 'DENY'
# ASP.NET Core - add in Program.cs
app.Use(async (ctx, next) => {
ctx.Response.Headers.Add("X-Frame-Options", "DENY");
await next();
});
// 07 Prevention: CSP frame-ancestors directive
Content-Security-Policy: frame-ancestors is the modern, OWASP-recommended replacement for X-Frame-Options. It is more powerful: it supports multiple allowed origins, wildcards at the scheme or host level, and is processed before X-Frame-Options in browsers that support both. Where X-Frame-Options: ALLOW-FROM is deprecated and ignored by Chrome and Firefox, frame-ancestors handles the multi-origin use case correctly.
HTTP HEADERS - CSP frame-ancestors# Block all framing - equivalent to X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none';
# Allow framing only by same origin - equivalent to SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self';
# Allow framing by self and specific trusted partner domains
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com https://embed.trusted.io;
# Combine with other CSP directives in one header
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; script-src 'self';
For applications that legitimately need to be embedded - such as a widget served to third-party sites - use frame-ancestors with an explicit allowlist of trusted origins. Never use a wildcard (*) as the frame-ancestors value; it provides no protection against clickjacking.
"X-Frame-Options tells the browser what it can't do. CSP frame-ancestors tells the browser exactly what it can do, and refuses everything else. The second posture is always stronger."
CyberFortify Web Application Security Practice// 08 Prevention: Frame busting, SameSite cookies, and confirmation dialogs
These three controls are defence-in-depth measures - they reduce clickjacking risk but are not reliable standalone fixes. Deploy them alongside the header-based controls above.
JavaScript frame busting
Frame busting scripts detect when the page is loaded inside a frame and redirect the top-level window to the page's own URL, breaking out of the iframe. The limitation is critical: they can be neutralised by the attacker using the sandbox attribute on the iframe, which blocks JavaScript execution in the framed context.
JAVASCRIPT// Basic frame busting - breaks out of any iframe
if (window.top !== window.self) {
window.top.location = window.self.location;
}
// Bypass used by attackers - sandbox prevents the JS from running:
// <iframe sandbox="allow-forms allow-scripts" src="target.html"></iframe>
// sandbox="allow-forms" alone lets forms submit but blocks top-frame navigation
// Result: frame buster silently fails, iframe renders normally
// Conclusion: frame busting is NOT a substitute for X-Frame-Options / CSP
SameSite cookies
The SameSite cookie attribute limits when session cookies are sent on cross-site requests. With SameSite=Strict, the session cookie is not sent when the page is loaded inside a cross-site iframe - meaning the attacker's decoy page cannot leverage an authenticated session from a different site. This reduces the impact of many clickjacking attacks but does not prevent all variants (same-site attackers, or scenarios where the victim navigates directly).
HTTP - SET-COOKIE# Strict - session cookie never sent cross-site (strongest)
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
# Lax - sent on top-level navigations only, not subframe loads (default in modern browsers)
Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly
# None - sent in all cross-site contexts (dangerous - requires Secure)
Set-Cookie: sessionId=abc123; SameSite=None; Secure; HttpOnly
# SameSite=None provides zero clickjacking protection
Confirmation dialogs on critical actions
Requiring explicit typed confirmation (not just a single click) on destructive or high-value actions raises the bar for clickjacking exploitability. An attacker cannot easily sequence multi-step dialog interactions through a single click. This is a useful UX control for actions like account deletion, wire transfers, and permission grants - even when header-based framing controls are in place.
HTML + JS<!-- Typed confirmation for destructive actions -->
<button onclick="confirmDelete()">Delete Account</button>
<script>
function confirmDelete() {
const typed = prompt('Type DELETE to confirm account deletion:');
if (typed === 'DELETE') {
// proceed with deletion
}
}
// Clickjacking can fire a click but cannot simulate a keyboard entry
// in an isolated iframe - making multi-step attacks significantly harder
</script>
// 09 Comparing clickjacking defences
| Defence | Browser support | Stops basic clickjacking | Sandbox iframe bypass | Notes |
|---|---|---|---|---|
X-Frame-Options: DENY |
Universal | Yes | Not bypassed | Simple, widely supported - use as fallback |
CSP frame-ancestors 'none' |
All modern | Yes | Not bypassed | OWASP recommended primary control - use this |
| JavaScript frame busting | Universal | Yes | Bypassed by sandbox | Supplement only - not reliable as primary |
SameSite=Strict cookies |
All modern | Reduces impact | Not bypassed | Limits session availability in iframe - defence-in-depth |
| Confirmation dialogs | N/A | Raises bar only | Mitigated | UX control - use on Critical actions regardless |
X-Frame-Options: ALLOW-FROM |
Deprecated | No (ignored by Chrome/Firefox) | N/A | Do not use - provides false sense of security |
// 10 Clickjacking prevention checklist
Use this checklist as a remediation reference after a pen test finding, or as a pre-launch hardening step for any web application that handles authenticated actions. This checklist aligns with OWASP Top 10 A05 (Security Misconfiguration) guidance and covers the controls expected in compliance frameworks including PCI DSS and ISO 27001.
CSP frame-ancestors on all responses
Set Content-Security-Policy: frame-ancestors 'none' (or 'self' if self-embedding is needed) on every HTTP response - not just HTML pages. Apply globally at the web server or API gateway level, not in application middleware that might be skipped on error paths.
X-Frame-Options as legacy fallback
Send X-Frame-Options: DENY alongside the CSP header. Older browsers that don't support frame-ancestors will honour this header instead. Two lines of server config, zero application code changes.
SameSite=Strict on session cookies
Set SameSite=Strict on all session and authentication cookies. This prevents the cookie from being sent when the target page is embedded in a cross-site iframe, removing the authenticated session from the clickjacking scenario entirely.
Remove any ALLOW-FROM directives
Audit existing configurations for X-Frame-Options: ALLOW-FROM. This directive is unsupported in Chrome and Firefox and provides no protection in those browsers. Replace with CSP frame-ancestors with an explicit origin allowlist.
Require explicit confirmation on critical actions
Identify your highest-impact single-click actions - fund transfers, account deletion, admin grants, OAuth approvals. Add a confirmation step that requires typed input or a separate verification code. This is the last line of defence if framing headers are accidentally removed in a deployment.
Include clickjacking in regression testing
Add a header assertion to your CI pipeline or post-deployment smoke test: verify that X-Frame-Options and Content-Security-Policy are present in the response headers of every sensitive endpoint. A missing header after a framework upgrade or nginx config change should fail the build, not a pen test.