Clickjacking tricks you into clicking a button on a hidden page underneath the one you see. Here's how it works and how to reliably prevent it (no attack steps).
What gets targeted
Anything that is "one click, done, and valuable."
| Likely target | Why |
|---|---|
| Confirming a transfer/purchase | One click moves money |
| Settings change (visibility, connected apps) | Sets up later takeover or data access |
| OAuth / permission "Allow" | Silently approves an account link |
| Social follow / like / post | Abused for spread and like-farming |
Why it works
Browsers can load another site in an iframe and stack it on top. The attacker makes that iframe transparent (opacity 0), so the user only sees the decoy UI placed below. When the user clicks the "fake button," the real button layered directly above gets the click.
It resembles CSRF, but CSRF sends a request behind the scenes, while clickjacking makes the user operate the real screen — so CSRF tokens alone don't stop it.
Real case: a browser plugin's permission dialog (2008)
In October 2008, Adobe published a security advisory (APSA08-08) about a clickjacking issue affecting multiple browsers. According to press reports, by layering the Flash Player settings dialog invisibly, a site could get a user to grant it access to the camera and microphone without realizing. Adobe released fixed versions of Flash Player and, as an interim workaround, advised users to choose "always deny" in the Settings Manager.
The lesson: transfer buttons aren't the only target. Any screen where a single click hands over a permission ("Allow," "Agree," "Connect") is in scope. MITRE classifies this weakness as CWE-1021 (Improper Restriction of Rendered UI Layers or Frames).
Defense: don't let other sites embed your site
Set CSP frame-ancestors (most important)
Add Content-Security-Policy: frame-ancestors 'self' (only your own origin may frame you). If no one needs to embed you, 'none' is safest. List domains only for the partners you explicitly allow.
Add X-Frame-Options too (backward compatible)
For older clients, also send X-Frame-Options: DENY (or SAMEORIGIN). This covers both old and new browsers.
Add a confirmation step for critical actions
Gate transfers and permission grants behind re-auth, a confirm dialog, or an intent check — harder to drive through a silent overlay.
Make session cookies SameSite
SameSite=Lax/Strict curbs cross-site auto-sending. Not a complete clickjacking fix on its own, but it weakens the broader "action started from another site" class.
X-Frame-Options vs CSP frame-ancestors
Both headers stop other sites from embedding yours, but they differ in the details.
| X-Frame-Options | CSP frame-ancestors | |
|---|---|---|
| Values | DENY (block all) / SAMEORIGIN (same origin only) | 'none' / 'self' / a list of allowed domains |
| Multiple allowed embedders | Not possible (ALLOW-FROM doesn't work in modern browsers) | Possible |
| Set via meta tag | No effect | No effect |
| When both are present | Ignored by supporting browsers | Takes precedence |
| Role | Fallback for older browsers | The main defense today |
When sending both, keep their meaning aligned: pair frame-ancestors 'none' with DENY, and frame-ancestors 'self' with SAMEORIGIN.
Common configuration mistakes
- Assuming CSP
default-srccovers it.frame-ancestorsdoes not fall back todefault-src. Even withdefault-src 'none', embedding stays allowed unless you writeframe-ancestors - Putting it in a meta tag:
<meta http-equiv>has no effect here, so send it as a response header - Leaving it as
Content-Security-Policy-Report-Only: that only reports violations and doesn't block embedding. Switch to the enforcing header once you've confirmed it works - Setting it on only some pages: people check the home page and miss the login page, settings pages, or error pages. Also confirm a CDN or proxy isn't stripping the header
How to check your own site
Check with a headers tool
Enter your site's URL in the security headers checker and see whether X-Frame-Options and CSP frame-ancestors are returned. Check not just the home page but also logged-in settings pages and the pages for transfers or permission grants.
Check in the browser's developer tools
Reload the page with the Network tab open and look at the HTML response headers. Also check that the header isn't sent twice or with conflicting values.
Set it in one place
Adding it to every response in one place (the web server such as nginx or Caddy, the reverse proxy, or the framework) prevents both gaps and duplicates. If you're writing a new CSP, the CSP builder can assemble one that includes frame-ancestors.
This site's view: one header line is the most effective fix, so check your own site first
Clickjacking defense isn't complex code; it's one or two response-header lines that block embedding. Apply them site-wide at the framework or reverse proxy (Caddy/nginx) so nothing slips through. The most common failure is a header you thought you had set but is actually missing, so after setting it, confirm with the steps above that it's really being returned.
Sources (primary)
- OWASP, "Clickjacking Defense Cheat Sheet" — cheatsheetseries.owasp.org (frame-ancestors, X-Frame-Options, SameSite; ALLOW-FROM no longer works; meta tags have no effect; limits of frame busting)
- MDN, "CSP: frame-ancestors" — developer.mozilla.org (allowed values, no default-src fallback, not supported in meta)
- RFC 7034, "HTTP Header Field X-Frame-Options" — rfc-editor.org
- IPA, "How to Secure Your Web Site, 1.9 Clickjacking" (Japanese) — ipa.go.jp
- Adobe, "Security Advisory for Flash Player (APSA08-08)" (October 7, 2008) — adobe.com
- Computerworld, "Clickjackers could hijack webcams, microphones, Adobe warns" (October 2008) — computerworld.com (the camera and microphone permission dialog was the target)
- MITRE, "CWE-1021: Improper Restriction of Rendered UI Layers or Frames" — cwe.mitre.org
Read next
- Glossary: What is CSRF · What is XSS
- Guide: Choosing security headers (what's still needed, what's obsolete)
- Check: Security headers checker · CSP builder