X-Frame-Options vs CSP frame-ancestors
Published June 2026 · Updated October 2026 · sourced from the HTML and CSP specifications and MDN
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 current browsers (not IE) |
| 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. That is enough for most sites, but not for sites that need to allow specific third-party embeds.
Per the HTML spec, SAMEORIGIN compares the page against every ancestor frame, not just the top-level page. Older browsers checked only the top-level page (RFC 7034 documents this), so an attacker frame sandwiched between two of your own pages slipped through. Chrome checks every ancestor from version 61 and Firefox from 59.
Conflicting values fail closed. If the header carries more than one value and any of them is DENY or SAMEORIGIN (for example X-Frame-Options: DENY, SAMEORIGIN, or the header sent twice), the spec says to block the framing. Several unrecognized values are treated as if the header were absent.
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.comIt has been obsolete for years. Chrome and Safari never supported it, Firefox removed it in version 70, and Edge dropped it with the move to Chromium. The HTML spec says it is "not to be implemented", and MDN notes that modern browsers ignore the header completely when they see it.
The browser does not skip just the directive. It ignores the whole header. A site that sends only ALLOW-FROM has no clickjacking protection in any modern browser, because the spec treats a lone invalid value as if the header were never sent.
This is the most common false sense of security with X-Frame-Options. The header shows up in curl output, scanner reports and dev tools, and it does nothing.
CSP frame-ancestors
Part of Content Security Policy Level 2, standardized in 2016. Every current browser supports it (Baseline Widely available since January 2018). Internet Explorer never did.
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors 'self' https://partner.comframe-ancestors accepts multiple origins and wildcard hosts. That is the practical difference: if three partner domains embed your widget, one CSP header covers them. 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. The CSP specification states that it does not fall back to default-src, so a policy declaring default-src 'none' still lets anyone embed the resource.
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 your page sits inside two other frames (an attacker page embeds an intermediate page that embeds yours), frame-ancestors checks both ancestors. If any ancestor does not match, the load is blocked.
This is normally what you want. It matters when you allow selective framing. If you set frame-ancestors https://partner.com, your page can be embedded by https://partner.com, but if a partner page containing your frame is itself framed by another site, your page is blocked, because that outer site is an ancestor too. Every level must match an entry in your policy.
Sharp edge: neither header works in <meta> tags
MDN and the specs agree:
<meta http-equiv="X-Frame-Options" content="deny">has no effect. X-Frame-Options is only enforced via HTTP headers.frame-ancestorsmust be ignored in a CSP delivered via a<meta>element (not supported), as arereport-uriandsandbox.
Both must come from the server. If you cannot set HTTP response headers (static hosting with no config, some CMS setups), you cannot use these mechanisms against clickjacking.
Sharp edge: Report-Only does not enforce
A Content-Security-Policy-Report-Only: frame-ancestors 'none' header sends violation reports but blocks nothing. It also does not trigger the precedence rule below, so X-Frame-Options still applies. Only an enforced Content-Security-Policy header counts.
If you send several CSP headers, every policy is enforced, so each one's frame-ancestors must allow every ancestor. Extra policies can only tighten the result.
What happens when both headers are present
The HTML and CSP specifications both say that frame-ancestors overrides X-Frame-Options. If a response carries an enforced CSP containing a frame-ancestors directive, the browser skips X-Frame-Options entirely. It does not combine the two or pick the stricter one.
So a defense-in-depth setup with mismatched headers can backfire:
# Looks like defense in depth, but it is not:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'self' https://partner.com
# Modern browsers enforce frame-ancestors and skip DENY entirely.
# partner.com can embed the page, even though X-Frame-Options says DENY.The same applies to a permissive policy. This page is not protected in any modern browser:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors *
# frame-ancestors * allows every origin, and DENY is never consulted.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, so set only frame-ancestors. Browsers without CSP frame-ancestors support (IE 11, Safari before 10) will then allow all framing. Only IE and pre-Chromium Edge ever honored ALLOW-FROM, so there is no practical fallback for them.
Which should you use?
For most sites, set both:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENYCSP frame-ancestors covers every current browser. X-Frame-Options only matters for the few old browsers (IE 11, Safari before 10) that support it but not CSP Level 2. The extra header costs one line.
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. Modern browsers ignore it and it offers no protection.