X-Frame-Options vs frame-ancestors, which clickjacking control to use
CentralCSP Team ·
Last update:
Both answer the same question: who is allowed to put your page inside a frame,
which is the control that stops clickjacking. The frame-ancestors
directive in Content Security Policy (CSP) is the modern, more expressive one;
X-Frame-Options
is the older header kept for clients that never implemented CSP. Send both. They
cannot conflict: any browser that enforces frame-ancestors is required by the
CSP spec to ignore X-Frame-Options, so the CSP directive decides on modern
browsers and the older header covers the rest.
The clickjacking threat
Clickjacking is an attack where the attacker loads your page inside a frame on a site they control, usually invisible or transparent, and lays their own bait over it. The victim thinks they are clicking a button on the attacker's page, but the click lands on your framed page: confirm a payment, change a setting, approve a permission. The defense is to tell the browser which sites, if any, may frame your page. Both controls below do exactly that.
X-Frame-Options, the original header
X-Frame-Options is the original anti-clickjacking header, documented in RFC
7034. It has two values that still work.
DENY: no site may frame the page. MDN's wording is that the page "cannot be loaded in any frame, regardless of origin", so not even your own site can frame it.SAMEORIGIN: only pages from the same origin may frame it.
X-Frame-Options: DENYOne nuance on SAMEORIGIN. Modern browsers check every ancestor in the frame
chain, so a same-origin frame nested inside a hostile page is still blocked. RFC
7034 documented that early implementations checked only the top-level browsing
context, which left that nesting trick open; current engines close it.
Despite its age, the header is not deprecated. In January 2025 the HTML spec
editors deliberately removed the words "legacy" and "obsoleted by" from its
description of the header, and the CSP spec now says frame-ancestors
"overrides" X-Frame-Options rather than "obsoletes" it. Every browser still
enforces it, and there are no plans to remove it.
ALLOW-FROM is obsolete, and it fails open
The header once had a third value, ALLOW-FROM uri, meant to permit a single
named origin. It started as an Internet Explorer extension, was never uniformly
implemented (RFC 7034's own words), could only ever name one origin with no
wildcards, and Firefox, which had also shipped it, removed it in 2019 and
pointed at frame-ancestors as the replacement.
The dangerous part is how it fails. A browser that does not recognize
ALLOW-FROM treats the value as invalid and drops the entire header. Chromium's
console message spells it out: "'ALLOW-FROM' is not a recognized directive. The
header will be ignored." So a site that ships only
X-Frame-Options: ALLOW-FROM https://partner.example.com today has no framing
restriction at all in modern browsers, which is failing open; OWASP warns that a
site depending on ALLOW-FROM has no clickjacking defense in browsers that do
not support it. If you need to allow specific framers, that is a job for
frame-ancestors.
Conflicting values fall back to deny
A related gotcha for layered infrastructure. If two layers set different values,
say the application sends SAMEORIGIN and a CDN adds DENY, the browser
receives conflicting X-Frame-Options values. Chromium treats that as an error
and falls back to deny, refusing to display the page in any frame. When
same-origin framing that used to work breaks after an infrastructure change,
duplicated headers are worth checking first.
frame-ancestors, the CSP replacement
frame-ancestors arrived in CSP Level 2 and is the directive that takes over
the job. Instead of fixed keywords, it takes a source list: 'none', 'self',
and any number of host or scheme sources, including wildcard subdomains like
*.example.com. That is what makes it more expressive. You can allow several
trusted partners at once, an entire subdomain tree, or a specific scheme, none
of which X-Frame-Options can express. Nonces and hashes do not apply to it,
since it lists framing parents, not scripts. The browser checks every ancestor
in the frame chain, and if any one of them fails the list, the load is
cancelled.
Content-Security-Policy: frame-ancestors 'self' https://partner.example.comTwo deployment rules matter more than anything else about it.
First, it has no fallback: default-src does not cover it. MDN is explicit that
"a policy that declares default-src 'none' still allows the resource to be
embedded by anyone". A locked-down CSP without an explicit frame-ancestors
line says nothing about framing, so set it explicitly.
Second, it is header-only. The CSP spec excludes frame-ancestors (along with
report-uri and sandbox) from <meta http-equiv="Content-Security-Policy">
delivery, so browsers ignore it in a meta tag. Set it at the server or CDN. If
you want the clickjacking protection without adopting a full policy, see
how to set frame-ancestors without a full CSP header.
Support is not a real constraint anymore: every modern engine (Blink, Gecko,
WebKit) enforces frame-ancestors. The one notable gap is Internet Explorer,
which never implemented it and only understands X-Frame-Options.
Precedence, the exact rule
The CSP spec settles what happens when both headers arrive on the same
response: "If a resource is delivered with a policy that includes a directive
named frame-ancestors and whose disposition is 'enforce', then the
X-Frame-Options header MUST be ignored." Chromium implements that literally; its
ancestor throttle lets frame-ancestors decide instead of X-Frame-Options
whenever the response carries an enforced frame-ancestors directive.
Note the word "enforce". Only an enforced frame-ancestors switches
X-Frame-Options off. If you are trialing the directive in a
Content-Security-Policy-Report-Only
header, it reports violations but does not
suppress X-Frame-Options, so the older header keeps enforcing while you watch
the reports. That is exactly the behavior you want during a rollout.
This precedence rule is why sending both is safe. There is no state where the
two fight each other: browsers that understand frame-ancestors use it and
drop X-Frame-Options, and older clients that predate CSP Level 2 read
X-Frame-Options alone. OWASP's clickjacking guidance recommends stacking
independent defenses for the same reason.
X-Frame-Options vs frame-ancestors side by side
| Capability | X-Frame-Options | frame-ancestors |
|---|---|---|
| Allow no framing at all | DENY | 'none' |
| Allow same-origin framing only | SAMEORIGIN | 'self' |
| Allow specific external origins | Not possible (ALLOW-FROM obsolete, fails open) | 'self' https://partner.example.com |
| Wildcard subdomains | Not possible | *.example.com |
| Ancestors checked | Whole chain in modern browsers (top-level only in early implementations) | Whole chain, any failing ancestor cancels the load |
| Set via meta tag | No effect (header only) | No effect (header only) |
Covered by default-src | Not applicable | No, must be set explicitly |
| When both are sent | Ignored where enforced frame-ancestors is present | Takes precedence when enforced |
| Support | All engines, including old IE | All modern engines (Blink, Gecko, WebKit); never IE |
Recommendation
Ship both, with the strictest setting your site allows. Use 'none' and DENY
if nothing should ever frame your page; use 'self' and SAMEORIGIN if your
own app frames itself.
Content-Security-Policy: frame-ancestors 'none'X-Frame-Options: DENYNever use ALLOW-FROM, and remove it where you find it, since it can leave a
page with no protection at all. If you need to allow named partners, express
that in frame-ancestors only and keep X-Frame-Options at SAMEORIGIN or
DENY for the old clients that cannot read the allowlist anyway. You can
confirm both headers are present and correctly formed with CentralCSP's free
security headers scanner.
The two headers also differ after deployment, not just in syntax: frame-ancestors
violations are reported through the Reporting API, and X-Frame-Options refusals are
not reported at all. Pointing the CSP at a CentralCSP endpoint means a partner site
that suddenly cannot embed you shows up as a csp-violation naming the framing origin,
rather than as a support ticket.
FAQ
Is X-Frame-Options deprecated?
No. Neither MDN nor the browser compatibility data marks it deprecated, and
every browser still enforces it. In January 2025 the HTML spec editors removed
the words "legacy" and "obsoleted by" from its description, and the CSP spec now
says frame-ancestors "overrides" it rather than "obsoletes" it. Only the
ALLOW-FROM value is obsolete. Keep sending the header alongside
frame-ancestors.
Should I use both X-Frame-Options and frame-ancestors?
Yes. They cannot conflict, because the CSP spec requires any browser enforcing
frame-ancestors to ignore X-Frame-Options. So the CSP directive decides on
every modern browser, and X-Frame-Options covers old clients such as Internet
Explorer that never implemented CSP. OWASP recommends layering independent
clickjacking defenses, and sending both headers costs one line each.
Why is X-Frame-Options ALLOW-FROM not working?
Because it is obsolete. It began as an Internet Explorer extension, was never
uniformly implemented elsewhere, and Firefox removed it in 2019. Worse, a
browser that does not recognize ALLOW-FROM ignores the whole header, so the
page fails open with no framing protection. Replace it with
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com.
Can I set frame-ancestors in a meta tag?
No. Browsers ignore frame-ancestors in a meta tag, and a meta tag does not
work for X-Frame-Options either, so set both at the server or CDN. For the
details and the deployment options, see
how to set frame-ancestors without a full CSP header.
Does frame-ancestors in Report-Only mode disable X-Frame-Options?
No. The precedence rule applies only to a policy "whose disposition is
'enforce'", so a frame-ancestors directive in a
Content-Security-Policy-Report-Only header reports violations while
X-Frame-Options keeps enforcing. That makes Report-Only a safe way to test an
allowlist before switching it on.
Related reading
- frame-ancestors and X-Frame-Options, the reference pages.
- How to set frame-ancestors without a full CSP header, when you want the clickjacking protection but not the rest of a policy yet.
- Which legacy headers to retire,
where
X-Frame-Optionssits among the other headers browsers have moved past. - Improve your security headers grade, the full checklist this pair of headers belongs to.
Sources
- CSP Level 3, relation of frame-ancestors to X-Frame-Options
- CSP Level 3, the frame-ancestors directive
- CSP Level 3, meta element delivery exclusions
- RFC 7034, HTTP Header Field X-Frame-Options
- MDN, X-Frame-Options
- MDN, CSP frame-ancestors
- MDN, Firefox release notes (ALLOW-FROM removal)
- OWASP, Clickjacking Defense Cheat Sheet
- Chromium, ancestor_throttle.cc (X-Frame-Options and frame-ancestors handling)
- whatwg/html PR 10938 and w3c/webappsec-csp PR 702, the 2025 wording changes