TulipLot works best in Chrome or a Chromium-based browser. Some features may be limited here.
← All posts
Security · · 10 min read

Clickjacking, the attack that framing headers exist to stop

Checked on 2026-08-24.

Most of the web apps people actually want to embed refuse to be put inside an iframe. Of the 60 we measured on 2026-08-24 for can I embed this site in an iframe?, 53 blocked it and 7 allowed it. That ratio looks like paranoia until you know what the refusal is for.

It's for one attack — old, simple, and still working wherever the defenses are missing.

The attack in one sentence

RFC 7034, the document that wrote down X-Frame-Options, defines it precisely: clickjacking "occurs when an attacker uses multiple transparent or opaque layers in the user interface to trick a user into clicking on a button or link on another page from server B when they were intending to click on the same place of the overlaying page from server A."

OWASP, which catalogs it under the alternate name "UI redress attack," puts the same thing in the language of consequences: the attacker is "'hijacking' clicks meant for their page and routing them to another page, most likely owned by another application, domain, or both."

The important word in the RFC's version is intending. The click is real. Your finger moved, and the browser dispatched a genuine, user-initiated event to a real page. Nothing was forged. You were just looking at the wrong thing.

What it looks like, built up piece by piece

We'll describe this in prose rather than ship a working page. There's no reason to hand out a template.

Start with the attacker's page. It's ordinary: some copy, an image, and a big button promising something worth a click — a prize, a captcha step, a "click to continue" gate.

Now add an iframe pointing at a real application, one the visitor's probably signed into. Give it absolute positioning so it can go anywhere on the page, and set its opacity to zero. It's invisible but not unrendered; display: none would do that, and a page nobody renders receives no clicks. It's fully laid out, fully live, and fully transparent.

Next comes the alignment, the only fiddly part. The attacker loads the real application in a normal tab and notes where the control worth stealing sits. They can't scroll the framed copy — a cross-origin contentWindow doesn't expose that — so they wrap the iframe in a small overflow: hidden box and shift it inside by negative offsets until the wanted control is the part showing. A fragment in the src does a coarser version of the same job.

Finally, the stacking. The invisible frame goes above the decoy in the paint order, so it is the frame — not the attacker's own button — under the cursor. The visitor sees the decoy, because the layer covering it is transparent. The browser sees the frame, because it is on top.

The visitor clicks. The event goes to the real application, in a real session, and the application does what it was asked. The attacker's page never reads a byte of the response, and doesn't need to.

Why the same-origin policy does not save you

This surprises people who assume cross-origin isolation covers it.

The same-origin policy stops the attacker's script reading the framed page: it can't see the DOM, the text, the cookies, or the response. All of that holds. What it doesn't stop is the framed page being positioned, sized, clipped, or made transparent — those are properties of the container, and the container belongs to the attacker.

So the attacker works blind and still wins. They don't need a view of the page, only the victim's pointer landing on a known coordinate.

Variants worth recognizing

Keystroke theft. OWASP records the same trick applied to typing: "With a carefully crafted combination of stylesheets, iframes, and text boxes, a user can be led to believe they are typing in the password to their email or bank account, but are instead typing into an invisible frame controlled by the attacker."

Likejacking. OWASP documents real campaigns against social features — attacks "abusing Facebook's 'Like' functionality" that could "trick logged-in Facebook users to arbitrarily like fan pages, links, groups," and a Twitter worm where the stolen click "caused them to re-tweet the location of the malicious page, and propagated massively." The informal name for that family is likejacking: the same attack with a cheap payload and a viral loop attached.

Cursorjacking. This attacks the pointer rather than the page: hide the real cursor and draw a fake one at an offset, so what the visitor aims at and what the browser clicks are different places. We describe the mechanism rather than any incident, because we could not verify the usual attributions from a primary source, and browser handling of custom cursors has changed since.

Underneath all three is one shape: the victim's intent is captured somewhere other than where they're looking.

Three defenses, strongest first

OWASP's Clickjacking Defense Cheat Sheet counts the two headers as one mechanism, then SameSite cookies, then a frame-buster. The headers aren't equally strong, so each gets an entry here, and SameSite has a section of its own below.

1. frame-ancestors. A Content Security Policy directive naming who may frame you, with 'none' meaning nobody. OWASP is blunt: frame-ancestors 'none' "is recommended unless a specific need has been identified for framing." It's the strongest because the browser refuses the load before the page exists, and because it takes a guest list rather than a yes-or-no switch. The frame-ancestors directive covers the syntax and the four ways a source list quietly matches nothing.

2. X-Frame-Options. The legacy header, two working values. It's weaker for one reason: it fails open. A browser meeting a value it doesn't recognize behaves as though the header were never sent, so a stale ALLOW-FROM line offers no protection while looking like it does. Send it alongside the directive for old clients, and know that current browsers ignore it whenever an enforced frame-ancestors is present. X-Frame-Options explained has the history.

3. A frame-buster script. JavaScript that checks whether it's the top window and, if not, breaks out. OWASP still publishes one for legacy browsers, and its shape shows how carefully it must be written: a <style> block setting body { display: none !important; }, removed by script only when self === top, and otherwise top.location = self.location. The page stays invisible until it's proved it isn't framed. That ordering is deliberate: the naive one-liner, if (top != self) top.location.href = self.location.href, appears in the cheat sheet under "Insecure Non-Working Scripts DO NOT USE."

Third place was earned long before the headers were standardized. RFC 7034 cites a Stanford study, "Busting frame busting: a study of clickjacking vulnerabilities at popular sites," and summarises it in one sentence: "existing anti-clickjacking measures, e.g., frame-breaking JavaScript, have weaknesses that allow their protection to be circumvented."

OWASP lists several. Double framing: put the victim inside two nested frames, and reading parent.location becomes a security violation that stops the counter-navigation. The onbeforeunload event: the framing page registers a handler that prompts the visitor, who cancels the navigation the frame-buster started. No-content flushing: repeatedly navigating the top window to a URL answering 204 No Content, which flushes the request pipeline and cancels the frame-buster's navigation with no prompt.

Why sandbox beats a frame-buster outright

There's a simpler defeat, and it needs no trickery.

A frame-buster is JavaScript. If the script doesn't run, the page doesn't bust out. OWASP files this under "Restricted zones" and gives the one-attribute version: <iframe src="http://www.victim.com" sandbox></iframe>.

The HTML Standard explains why that's enough. A sandbox attribute with no value sets the whole flag set, and among those flags is the sandboxed scripts browsing context flag, which "blocks script execution." Even if the script did run, two more flags wait: the sandboxed top-level navigation flags, each of which "prevents content from navigating their top-level browsing context" — precisely the move a frame-buster makes.

So an attacker who wants to neutralize a frame-buster adds one bare attribute to their iframe tag. That's the whole bypass, and it's the sharpest argument for the ranking above: the header defenses are enforced by the browser on the framed page's own instructions, and no attribute on the attacker's element overrides them. Every iframe sandbox token goes through the flags one by one.

Why SameSite cookies blunt the attack

This is OWASP's second mechanism, and it isn't a framing control at all. Its advice holds: "where possible more than one of them should be implemented in order to provide defense in depth."

Most payloads worth stealing need a live session — delete the messages, confirm the transfer, grant the app access. If the session cookie never reaches the framed page, that page is logged out and the stolen click lands on a login screen.

OWASP states it plainly: "Cookies with a SameSite attribute of either strict or lax will not be included in requests made to a page within an <iframe>. This means that if the session cookies are marked as SameSite, any Clickjacking attack that requires the victim to be authenticated will not work."

Note the condition, which OWASP names in the next breath: "If the Clickjacking attack does not require the user to be authenticated, this attribute will not provide any protection." A click that triggers an action available to anonymous visitors isn't touched by cookie rules. Treat SameSite as depth, not the wall. Cookies inside an iframe covers it in full, including why it also breaks legitimate embedding.

What the measured data shows

Look back at that 53-to-7 split and it reads differently. The sites refusing to be framed aren't being difficult; they're declining to be the bottom layer of somebody's sandwich. Our Gmail request ended on accounts.google.com, and that host answered X-Frame-Options: DENY — the right answer for a page that grants applications access to accounts. Why Gmail and Google Calendar never load in an iframe walks that chain hop by hop.

The thing to carry away is the failure mode. frame-ancestors and a modern X-Frame-Options value fail closed. Everything else here — an unrecognized header value, a frame-buster, a cookie attribute on an anonymous action — fails open, silently, while the configuration file still looks correct.

Questions

Does clickjacking still work in current browsers?

It isn't patched, because nothing about it is a browser bug: a transparent element over another is normal CSS, and a click on the top layer going to the top layer is normal behavior. What changed is that the defenses are widely deployed and easy to switch on, and cross-site cookie rules have tightened underneath. A site sending frame-ancestors 'none' can't be framed, so it can't be clickjacked this way. A site sending nothing can be.

Can my page protect a third-party site I put in a frame?

No, and the direction of the control is the reason. Framing permission belongs to the framed page's own response headers, so nothing in your HTML grants or withholds it. You can only restrict yourself: frame-src in your own policy limits what your page may load, and sandbox limits what a frame may do once loaded.

If I use frame-ancestors, do I still need X-Frame-Options?

Only for clients too old to understand the directive; current browsers ignore the header whenever an enforced frame-ancestors is present. Sending both is harmless, and several sites in our data set do it. The trap is the reverse case: if you send only the legacy header, check the value, because an obsolete one buys nothing.