All posts

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: DENY

One 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.com

Two 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

CapabilityX-Frame-Optionsframe-ancestors
Allow no framing at allDENY'none'
Allow same-origin framing onlySAMEORIGIN'self'
Allow specific external originsNot possible (ALLOW-FROM obsolete, fails open)'self' https://partner.example.com
Wildcard subdomainsNot possible*.example.com
Ancestors checkedWhole chain in modern browsers (top-level only in early implementations)Whole chain, any failing ancestor cancels the load
Set via meta tagNo effect (header only)No effect (header only)
Covered by default-srcNot applicableNo, must be set explicitly
When both are sentIgnored where enforced frame-ancestors is presentTakes precedence when enforced
SupportAll engines, including old IEAll 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: DENY

Never 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.

Sources