Back to issues
SECURITY

Clickjacking: The Attack That Hijacks Your Clicks (and How X-Frame-Options Protects You)

Understand how clickjacking works, why X-Frame-Options exists, and how to use CSP frame-ancestors to protect your site from being embedded in malicious iframes.

By Thiago Saraiva8 MIN

Mental Model Think of your website as a storefront window. An iframe is like someone pressing a two-way mirror against your glass from the outside. You still see your own display, but passersby see their fake display, and when they "tap the window," they're actually tapping your real merchandise underneath. X-Frame-Options is the sign on your door that says: "No mirrors allowed."

Picture a transparent sheet of paper laid over a contract. The paper says "I agree to enter the raffle." The contract underneath says "I transfer my house to John." You sign thinking it's the raffle. That's clickjacking.

And this isn't theory. It happens on the web constantly, and if you haven't configured a single HTTP header, your site might be vulnerable right now.

A Real Incident: The Twitter "Don't Click" Worm (2009)

Remember the two-way mirror from the intro? Here's what happens when nobody checks for them.

In February 2009, a button started showing up on random pages across the web labeled simply "Don't Click." Curiosity did its job. Users clicked, and a tweet appeared on their timeline: "Don't Click: http://tinyurl.com/amgzs6", which spread the same button to their followers' feeds. Nobody typed anything. Nobody authorized anything. They just clicked a button that looked harmless.

Under the hood? An invisible iframe of Twitter's status update form, positioned exactly over the decoy button. The user's active Twitter session did the rest. Twitter patched it within hours by adding frame-busting JavaScript, but the lesson stuck: if your site can be framed, someone will frame it.

Facebook's "Like" button faced the same issue for years (coined "likejacking"), and Adobe's Flash Player Settings Manager was famously vulnerable in 2008, letting attackers silently enable a victim's webcam and microphone. Same attack pattern. Different decorations on the mirror.

What Is an iframe (and Why Is It Dangerous)

An iframe loads an entire web page inside another page. The user might not even realize they're interacting with a different site.

The attacker builds an attractive page ("Win an iPhone!") and overlays an invisible iframe of your bank. The "Claim Prize" button sits exactly over the "Confirm Transfer" button. You click on what you see, the real action happens in the iframe.

It works because:

  • You're logged into the bank (active session cookie)
  • The iframe loads the bank with your authenticated session
  • The iframe is invisible but clickable
  • The click registers on the real iframe

Think of it like a ventriloquist. The puppet (the fake page) moves its mouth, but the voice (your click) comes from somewhere else entirely.

X-Frame-Options: The Direct Solution

X-Frame-Options is an HTTP header that tells the browser: "this page can or cannot be loaded inside an iframe."

X-Frame-Options: DENY

When the browser receives this header, it refuses to render the content inside any iframe. The iframe stays empty. Attack fails.

It's the "No mirrors allowed" sign from the mental model. The browser is the bouncer reading it.

The 3 Possible Values

  • DENY - Nobody can put me in an iframe. Not even myself.
  • SAMEORIGIN - Only pages from my own domain can.
  • ALLOW-FROM https://partner.com - Obsolete in 2026. Chrome and Safari never implemented it, Firefox dropped it in version 70. Use frame-ancestors instead.

CSP frame-ancestors: The Evolution

CSP frame-ancestors solves all the limitations of X-Frame-Options:

Gotcha: frame-ancestors only works in a real Content-Security-Policy response header. Setting it via <meta http-equiv="Content-Security-Policy"> is silently ignored by every browser. It's one of a handful of CSP directives that the meta tag can't deliver.

Which one to use? Both. CSP is the primary, X-Frame-Options is the fallback for older browsers and oddball WebViews:

Browser Compatibility at a Glance

BrowserX-Frame-OptionsCSP frame-ancestors
Chrome / EdgeFull supportFull support
FirefoxFull supportFull support
SafariFull support (ignores ALLOW-FROM)Full support
IE 11Partial (DENY, SAMEORIGIN only)Not supported
Legacy mobile WebViewsVariesOften missing

Rule of thumb: when both headers are present, modern browsers honor frame-ancestors and ignore X-Frame-Options. That's by design, the CSP directive wins. You still send both because you don't control which browser your visitor uses.

Clickjacking Variants You Might Not Know About

Likejacking: invisible Facebook Like iframe over an attractive button. You like spam without realizing it.

Cursorjacking: the visible cursor is offset from the real cursor. You think you're clicking one spot, but the click happens somewhere else.

Multi-step clickjacking: a "game" where each click hits a step in a real operation (Menu > Transfer > Confirm > Execute). Whack-a-mole where every mole you hit wires money somewhere.

When NOT to Lock Everything Down

Defaulting to DENY is right 95% of the time. The exceptions matter:

  • OAuth/SSO consent screens that some integrations expect to render in a popup. Keep these on SAMEORIGIN or a tight frame-ancestors allowlist, never DENY.
  • Embeddable widgets (chat, pricing tables, video players). These need a per-tenant allowlist, see the SaaS scenario below.
  • Internal tools behind a corporate dashboard that legitimately framing them. Audit the parent app, then allowlist its origin.

Apply DENY site-wide first, then carve narrow exceptions. Never the other way around.

The 3 Layers of Protection

Layer 1: HTTP Headers

Layer 2: Sec-Fetch Headers (modern server-side validation)

Modern browsers send headers that explicitly state the context of the request:

Layer 3: JavaScript frame-busting (last resort defense)

Hide the body first. If we're in an iframe, the body never appears. If we're not, remove the CSS. Fail-secure:

Why doesn't naive frame-busting (if (top !== self) top.location = self.location) work? Because the attacker can use sandbox="allow-scripts allow-forms" on the iframe, which silently blocks the redirect (the sandbox attribute strips allow-top-navigation by default).

How to Test Your Site for Clickjacking

You don't need a pentest suite. Five minutes and a terminal are enough.

1. Check the headers with curl

If nothing comes back, your site has no protection. That's the mirror waiting to happen.

Bonus, follow redirects (login pages often live one hop away):

2. Try to embed your site locally

Create a file called clickjack-test.html:

Open it in your browser. If your site renders inside the iframe, you're exposed. If the iframe is blank (and DevTools shows an error like "Refused to display... in a frame because it set 'X-Frame-Options' to 'deny'"), you're protected.

3. Verify in DevTools

Open Chrome DevTools > Network tab > reload the page > click any document request > Headers tab. Look for Content-Security-Policy and X-Frame-Options in the Response Headers section. If they're missing, ship the fix today.

4. Online scanners (optional)

Tools like securityheaders.com or Mozilla Observatory give you a letter grade in seconds. Good for sanity checks, not a replacement for testing the real response.

Real-World Scenario: SaaS with an Embeddable Widget

If you have an admin area that shouldn't be embedded, but a widget that clients need to embed:

FAQ

Q: My site doesn't handle money or logins. Do I still need this? Yes. Clickjacking isn't only about transfers. Attackers frame sites to inflate ad clicks, trigger "subscribe" buttons, toggle account settings, or plant reputation attacks. If your site has any button, it's a target.

Q: Do I need both X-Frame-Options and CSP frame-ancestors? Set both. Modern browsers prefer frame-ancestors and ignore X-Frame-Options when CSP is present, but older browsers (and weird WebViews) only understand the legacy header. Belt and suspenders.

Q: I want to allow only my own subdomains to embed the page. How? Use Content-Security-Policy: frame-ancestors 'self' https://*.mydomain.com. X-Frame-Options can't express wildcards, which is one of the reasons CSP replaced it.

Q: Will this break legitimate integrations like Stripe Checkout or YouTube embeds? No. These headers control who can embed your page, not who you can embed. Embedding other sites is controlled by their headers, not yours.

Q: I set the headers but attackers still embed my page. Why? Four common culprits: (1) headers set on the HTML but not on error pages or redirects, (2) a CDN or reverse proxy stripping them, (3) typo in the directive (frame-ancestor without the s is silently ignored, CSP treats unknown directives as no-ops), (4) you set frame-ancestors via a <meta> tag, which browsers ignore. Re-run the curl -I test above against the exact URL being framed.

Key Takeaways

  • Clickjacking makes the user click on something they don't intend to, using invisible iframes
  • X-Frame-Options: DENY is the bare minimum every site should have
  • CSP frame-ancestors is the modern and more powerful version, and it must be sent as a real HTTP header
  • Use both: CSP as primary, X-Frame-Options as fallback
  • Protect all pages by default, not just the "sensitive" ones
  • For embeddable widgets, use frame-ancestors with a specific allowlist
  • Don't forget object-src 'none' in your CSP (attackers can use <object> instead of iframes)