Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- X-Frame-Options and CSP's frame-ancestors directive both prevent clickjacking by controlling how your page can be embedded in iframes. But they are not equivalent — one is a legacy header, the other is the modern standard. This guide explains the differences, which to use, and why you should set both.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
X-Frame-Options vs CSP frame-ancestors: Which Header to Use in 2026?
Clickjacking is one of the oldest tricks in the web attacker''s playbook — and it still works on sites that have not added framing protection. Two HTTP security headers address this problem: the legacy X-Frame-Options and the modern Content-Security-Policy: frame-ancestors directive.
If you have looked at security scanner output and seen both flagged or both missing, you might wonder what the difference is and which one to implement. This guide answers that question with a clear explanation of clickjacking, a detailed comparison of both mechanisms, and specific recommendations for 2026.
What Is Clickjacking?
Clickjacking (also called "UI redressing") is an attack where a malicious page loads your site in a transparent or hidden iframe and tricks users into clicking on elements from your site while they think they are interacting with the attacker''s page.
A Classic Clickjacking Attack
- 1Attacker creates a page at
evil.comthat displays a tempting button: "Claim Your Prize" - 2Behind the button, at full transparency (opacity: 0), is an iframe of your site — specifically positioned over a "Confirm Transfer" or "Delete Account" or "Authorise App" button
- 3The user clicks "Claim Your Prize" and unknowingly clicks your site''s button
- 4Your site registers an authenticated action from the user''s session
The attack is particularly effective because: - It works entirely in the browser — no injection, no exploit - The user is authenticated on your site (their session cookies are sent with the iframe) - The user sees the attacker''s page, not your site - It is trivially easy to execute — a few lines of HTML and CSS
What Clickjacking Can Achieve
The impact depends on what actions are exposed. Documented real-world uses include: - Authorising OAuth application permissions - Transferring funds in banking applications - "Liking" social media posts (likejacking) - Deleting accounts or changing passwords - Tricking users into enabling microphone/camera permissions
X-Frame-Options: The Legacy Header
X-Frame-Options was introduced by Microsoft Internet Explorer in 2009 and quickly adopted across browsers. It tells browsers whether they are allowed to render the page in a <frame>, <iframe>, <embed>, or <object>.
Values
`X-Frame-Options: DENY`
The page cannot be embedded in any frame, by any origin. This is the strongest setting.
X-Frame-Options: DENY`X-Frame-Options: SAMEORIGIN`
The page can only be embedded in a frame on the same origin (same scheme + hostname + port). Frames from any other domain are blocked.
X-Frame-Options: SAMEORIGIN`X-Frame-Options: ALLOWFROM uri` (deprecated)
The page can be embedded in a frame from the specified URI. For example:
X-Frame-Options: ALLOWFROM https://trusted-partner.comThis value was supposed to allow per-origin framing control, but its implementation was inconsistent across browsers and it was never fully adopted. Chrome never supported it. Do not use it.
Limitations of X-Frame-Options
- No multiple origins: You cannot whitelist more than one specific external origin. You choose between "no framing" (DENY), "same-origin only" (SAMEORIGIN), or one specific origin (ALLOWFROM, which is broken).
- ALLOWFROM is broken: As noted, Chrome never implemented it.
- Cannot be used in `<meta>` tags: X-Frame-Options must be an HTTP response header — it cannot be set via a
<meta http-equiv>tag. Browsers ignore it in meta tags. - Spec gaps: Early specifications had ambiguity about what "same origin" means in edge cases involving ports and protocols.
Despite its limitations, X-Frame-Options is simple, well-understood, and supported everywhere.
CSP frame-ancestors: The Modern Standard
The frame-ancestors directive in Content Security Policy (Level 2, introduced in 2014) is the modern, standards-compliant replacement for X-Frame-Options.
Content-Security-Policy: frame-ancestors ''none''
Content-Security-Policy: frame-ancestors ''self''
Content-Security-Policy: frame-ancestors ''self'' https://partner.com https://admin.yourapp.comKey Advantages Over X-Frame-Options
Multiple origins: You can specify exactly which origins are allowed to embed your page, with no limit:
Content-Security-Policy: frame-ancestors ''self'' https://app.example.com https://dashboard.example.comProtocol and port specificity: Sources can be specified with full precision:
Content-Security-Policy: frame-ancestors https://example.com:8443Wildcard subdomain matching:
Content-Security-Policy: frame-ancestors https://*.yourapp.comScheme-level control:
# Allow any HTTPS origin (not recommended — but possible)
Content-Security-Policy: frame-ancestors https:Spec-compliant: frame-ancestors is part of the CSP specification (W3C standard), while X-Frame-Options was an informal proposal.
What frame-ancestors Does NOT Do
One critical distinction: frame-ancestors controls what can embed your page — it does not control what your page can embed. If you want to restrict which iframes your page loads (to prevent injected iframes), you use the frame-src directive.
# Control what can embed YOUR page:
Content-Security-Policy: frame-ancestors ''self''
# Control what YOUR PAGE can embed:
Content-Security-Policy: frame-src https://www.youtube.com https://player.vimeo.comBrowser Support Comparison
X-Frame-Options
Supported in every browser in production use today, including Internet Explorer 8+, all versions of Chrome, Firefox, Safari, and Edge. It is also respected by many HTTP proxies and security tools.
CSP frame-ancestors
Supported in all modern browsers: Chrome 40+, Firefox 36+, Safari 10+, Edge 15+.
Internet Explorer: Does not support CSP at all. If you have IE users (increasingly rare), X-Frame-Options is your only option.
Which header takes precedence?
The CSP Level 2 specification states that if a Content-Security-Policy header with a frame-ancestors directive is present, browsers that support CSP Level 2 should use frame-ancestors and ignore X-Frame-Options. In practice:
- Modern browsers (Chrome, Firefox, Safari, Edge) follow this: frame-ancestors wins
- Internet Explorer uses X-Frame-Options (it doesn''t understand CSP)
This means setting both headers gives you the correct behaviour in all browsers: CSP frame-ancestors is used in modern browsers, X-Frame-Options is the fallback for older environments.
Which Should You Use in 2026?
Set both.
Here is the reasoning:
- 1CSP frame-ancestors is the right standard going forward and gives you more control
- 2X-Frame-Options is the fallback for Internet Explorer and certain proxies/CDNs that inspect X-Frame-Options specifically
- 3Setting both has no downside — they do not conflict when set consistently
The only exception: if you need to whitelist multiple external origins, CSP frame-ancestors is the only way to do it. In this case, set X-Frame-Options: SAMEORIGIN as the legacy fallback (the strictest value X-Frame-Options can meaningfully provide), and use frame-ancestors for the precise control you need.
Implementation Examples
Nginx
# Block all framing (strongest — for pages that should never be embedded)
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors ''none''" always;
# Allow same-origin framing only
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors ''self''" always;
# Allow same origin plus specific partner
# (X-Frame-Options can''t express this precisely — use SAMEORIGIN as fallback)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors ''self'' https://partner.example.com" always;Note: If you already have a CSP header with other directives, you need to append frame-ancestors to the existing policy rather than setting a second Content-Security-Policy header:
add_header Content-Security-Policy "default-src ''self''; script-src ''self''; frame-ancestors ''none''" always;Apache
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors ''self''"
</IfModule>Node.js / Express
import helmet from ''helmet'';
import express from ''express'';
const app = express();
// Helmet sets both X-Frame-Options and can configure CSP
app.use(helmet({
frameguard: {
action: ''sameorigin'' // Sets X-Frame-Options: SAMEORIGIN
},
contentSecurityPolicy: {
directives: {
// ... other directives ...
frameAncestors: ["''self''"], // Sets frame-ancestors ''self''
},
},
}));Next.js
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: ''/(.*)'',
headers: [
{
key: ''X-Frame-Options'',
value: ''SAMEORIGIN'',
},
{
key: ''Content-Security-Policy'',
value: "frame-ancestors ''self''",
// Note: Merge this with your full CSP policy
},
],
},
];
},
};
module.exports = nextConfig;PHP
<?php
header(''X-Frame-Options: SAMEORIGIN'');
header("Content-Security-Policy: frame-ancestors ''self''");Meta Tag Workaround (Does Not Work)
You may see suggestions to use the meta tag equivalent:
<!-- THIS DOES NOT WORK FOR EITHER HEADER -->
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
<meta http-equiv="Content-Security-Policy" content="frame-ancestors ''self''">Neither X-Frame-Options nor CSP frame-ancestors work when set via meta tags. Browsers specifically require these to be HTTP response headers. The meta tag approach is ineffective — it appears to be set in the HTML source, but browsers ignore it for framing decisions. Set them at the server level.
Testing Your Framing Protection
Basic Test
Create a simple HTML page on a different domain (or use a local test page) and try to embed your site in an iframe:
<!-- test.html — serve from a different origin -->
<!DOCTYPE html>
<html>
<body>
<p>Can this iframe load yourdomain.com?</p>
<iframe src="https://yourdomain.com" width="800" height="600"></iframe>
</body>
</html>Open this in your browser. If your framing protection is working, the iframe will be blank and the browser console will show a framing error like:
Refused to display ''https://yourdomain.com/'' in a frame because it set ''X-Frame-Options'' to ''sameorigin''.or
Refused to frame ''https://yourdomain.com/'' because an ancestor violates the following Content Security Policy directive: "frame-ancestors ''none''".Automated Check
Run a free scan at ZeriFlow — it checks both X-Frame-Options and CSP frame-ancestors as part of its security header analysis, flagging missing or misconfigured values.
When Should You Allow Framing?
Some legitimate use cases require framing:
Embedded widgets: If you provide a widget that other sites embed (a chat widget, a booking form, a payment element like Stripe), you need to allow the specific origins that will embed your widget:
Content-Security-Policy: frame-ancestors ''self'' https://customerdomain.comSaaS dashboards in parent apps: If your SaaS can be embedded in a client''s parent application:
Content-Security-Policy: frame-ancestors ''self'' https://*.clientplatform.comYour own multi-domain product: If you have app.yourdomain.com and admin.yourdomain.com that embed each other:
Content-Security-Policy: frame-ancestors ''self'' https://app.yourdomain.com https://admin.yourdomain.comIn all these cases, be as specific as possible. Use frame-ancestors ''none'' for pages that should never be framed (login pages, settings, payment flows) and specific origin lists only for pages that have a genuine embedding use case.
Summary: Decision Tree for 2026
Should any external origin be allowed to embed this page?
- No → Set X-Frame-Options: DENY + Content-Security-Policy: frame-ancestors ''none''
Should only my own origin be allowed to embed this page?
- Yes → Set X-Frame-Options: SAMEORIGIN + Content-Security-Policy: frame-ancestors ''self''
Should specific external origins be allowed?
- Yes → Set X-Frame-Options: SAMEORIGIN (fallback) + Content-Security-Policy: frame-ancestors ''self'' https://trusted-origin.com
Is my site an embedded widget or embedded by design?
- Yes → Use only Content-Security-Policy: frame-ancestors ''self'' https://allowed-embedder.com; omit X-Frame-Options or set SAMEORIGIN as fallback
In all cases: prefer frame-ancestors for precision, keep X-Frame-Options for compatibility, and verify both are working with an automated scan at ZeriFlow.
See ZeriFlow in action — free scan.
80+ checks, zero false positives. No signup needed.