Clickjacking Prevention: X-Frame-Options vs frame-ancestors
Clickjacking Prevention: X-Frame-Options vs frame-ancestors
Clickjacking — also known as a UI redress attack — is when an attacker iframes your site transparently over their own UI and tricks the user into clicking "invisible" buttons: confirm a payment, reset a password, grant a permission. Clickjacking mitigation is one of the cheapest security fixes in existence, yet around 40% of sites still miss it. Let's walk through the attack and three defensive layers: X-Frame-Options, CSP frame-ancestors, and JS frame-busting.
How clickjacking works
The attacker creates a page with a transparent iframe of your site on top of fake UI. The user thinks they're clicking "Claim prize" but actually clicks "Delete account" in their authenticated session on your site. The canonical example is the 2009 Twitter clickjacking worm that forced one-click retweets of malicious content. Reference: OWASP Clickjacking.
X-Frame-Options (legacy but universal)
X-Frame-Options is defined in RFC 7034. Three values:
DENY— the page cannot be framed anywhereSAMEORIGIN— only from the same originALLOW-FROM uri— deprecated, not honoured by Chrome/Safari
X-Frame-Options: DENY
CSP frame-ancestors (modern standard)
The frame-ancestors directive in CSP Level 2+ replaces X-Frame-Options and supports multiple sources:
Content-Security-Policy: frame-ancestors 'none';
# or
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;
When both are present the browser honours CSP. Keep both for legacy clients.
SameSite cookies
Even if clickjacking succeeds, a cookie with SameSite=Lax or SameSite=Strict will not ride along on the cross-site iframe request — a second line of defence. Details in Cookie Security.
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict
JS frame-busting (do not rely on this alone)
if (self !== top) {
top.location = self.location;
}
This snippet breaks against the iframe sandbox attribute. Use it only as a fallback for browsers without CSP support.
Configuration
nginx:
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
Apache:
Header always set X-Frame-Options "DENY"
Header always set Content-Security-Policy "frame-ancestors 'none'"
Express/NestJS:
import helmet from 'helmet';
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.contentSecurityPolicy({
directives: { frameAncestors: ["'none'"] }
}));
When SAMEORIGIN is appropriate
If you legitimately iframe across subdomains (admin widgets, embedded apps), use SAMEORIGIN or frame-ancestors 'self'. Never use ALLOWALL — it's equivalent to no protection.
Verification
curl -I https://example.com | grep -E "X-Frame-Options|Content-Security"
Or use the enterno.io Security Scanner, which actually tries to iframe your site and reports whether framing is blocked. Broader overview: HTTP Security Headers.
FAQ
Do I still need X-Frame-Options with CSP? Yes, for browsers older than CSP Level 2 (IE 11) and as defence-in-depth.
Can I allow framing only from one domain? Only via frame-ancestors https://partner.com; X-Frame-Options ALLOW-FROM is dead.
How strong is frame-busting JS? Weak — attackers bypass via iframe sandbox or HTML5 double-framing.
What about mobile WebViews? WebViews don't always honour CSP; add server-side Referer/Origin validation as a backup.
Conclusion
Minimum: ship two headers and call it done. Monitor via enterno monitors so any regression in config surfaces immediately. See also: Cookie Security, all security headers.
Check your website right now
Check now →