X-Frame-Options vs CSP frame-ancestors
Published June 2025 · sourced from MDN and the CSP specification
Two HTTP headers prevent clickjacking. Most guides will tell you to set both and move on. This post covers the sharp edges those guides skip -- the behaviors that make a site look protected when it is not.
Quick comparison
| X-Frame-Options | CSP frame-ancestors | |
|---|---|---|
| Browser support | All browsers (IE8+) | All browsers since ~2018 |
| Block all | DENY | 'none' |
| Same origin only | SAMEORIGIN | 'self' |
| Specific origins | Not supported | 'self' https://partner.com |
| Default if missing | Allows all framing | Allows all framing |
| Works in <meta> tag | No | No |
X-Frame-Options
The original anti-clickjacking header. It has three directives, but only two work:
X-Frame-Options: DENY # Block all framing
X-Frame-Options: SAMEORIGIN # Allow only same-origin framingThat is the entire API. No multiple origins, no wildcards, no path-level control. For most sites this is enough. For sites that need to allow specific third-party embeds, it is not.
Sharp edge: ALLOW-FROM kills the entire header
The ALLOW-FROM directive was meant to allow framing from a single origin:
X-Frame-Options: ALLOW-FROM https://partner.comThis has been obsolete for years. Chrome and Safari never supported it. Firefox dropped support. MDN is explicit: "Modern browsers that encounter response headers with this directive will ignore the header completely."
Not "ignore the directive." Ignore the entire header. A site using ALLOW-FROM has no clickjacking protection in any modern browser. The header is treated as if it were never sent.
This is the most common false sense of security with X-Frame-Options. The header is present in curl output, in security scanner reports, in the response headers panel of browser dev tools -- and it does nothing.
CSP frame-ancestors
Part of Content Security Policy Level 2, standardized in 2016. Supported in all browsers since January 2018 (Baseline Widely Available).
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors 'self' https://partner.comframe-ancestors supports multiple origins and wildcards. This is the practical difference: if you have three partner domains that embed your widget, CSP handles it in one header. X-Frame-Options cannot.
Sharp edge: default-src does not cover framing
Many CSP policies start with default-src 'self' as a baseline. The assumption is that any directive not explicitly set falls back to default-src.
frame-ancestors is the exception. Per the CSP specification and MDN: "default-src fallback: No. Not setting this allows anything."
A policy of default-src 'none'; script-src 'self' looks locked down. It blocks everything except same-origin scripts. It still allows any site to embed the page in an iframe, because frame-ancestors is absent and default-src does not fill the gap.
If you care about clickjacking, you must set frame-ancestors explicitly. It is never inherited.
Sharp edge: frame-ancestors checks every ancestor
If a page is nested three frames deep (attacker page embeds an intermediate page that embeds your page), frame-ancestors checks all three ancestors. If any ancestor does not match, the load is blocked.
This is normally what you want. It becomes relevant when you allow selective framing. If you set frame-ancestors https://partner.com, your page can be embedded directly by https://partner.com, but not by a page that partner.com itself embeds. If the partner embeds your page inside their own iframe stack, every level in that stack must match an entry in your policy.
Sharp edge: neither header works in <meta> tags
Both MDN and the CSP specification are unambiguous:
<meta http-equiv="X-Frame-Options" content="deny">has no effect. X-Frame-Options is only enforced via HTTP headers.frame-ancestorsis explicitly listed among the CSP directives not supported in<meta>elements, alongsidereport-uriandsandbox.
These directives must come from the server. If your site cannot set HTTP response headers (static hosting with no config, some CMS setups), you cannot protect against clickjacking with these mechanisms.
What happens when both headers are present
The CSP specification says frame-ancestors obsoletes X-Frame-Options. When both headers are present on a response, browsers that support CSP Level 2 apply only frame-ancestors and ignore X-Frame-Options.
This means a carefully constructed defense-in-depth setup can backfire:
# Looks like defense in depth, but in practice:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'self' https://partner.com
# Modern browsers see frame-ancestors 'self' https://partner.com
# and ignore DENY entirely.
# Partner.com can embed the page, even though X-Frame-Options says DENY.The fix: make both headers say the same thing:
# Consistent policy:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'If you need to allow specific origins, you cannot express that in X-Frame-Options. Set only frame-ancestors in that case. The old browsers that do not support CSP Level 2 (IE11, very old Safari) will allow all framing, but for those browsers the X-Frame-Options alternative was ALLOW-FROM-- which those same browsers mostly ignored anyway.
Which should you use?
For most sites, set both:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYCSP frame-ancestors covers all modern browsers. X-Frame-Options covers the vanishingly small set of browsers that support XFO but not CSP Level 2. Setting both costs one extra header line and adds nothing to the response size that matters.
If you need to allow specific origins, use only CSP:
Content-Security-Policy: frame-ancestors 'self' https://partner.comDo not add X-Frame-Options: ALLOW-FROM. It will be ignored entirely by modern browsers and offers no protection.