Skip to content
ClickJack Test

← Blog

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-OptionsCSP frame-ancestors
Browser supportAll browsers (IE8+)All current browsers (not IE)
Block allDENY'none'
Same origin onlySAMEORIGIN'self'
Specific originsNot supported'self' https://partner.com
Default if missingAllows all framingAllows all framing
Works in <meta> tagNoNo

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 framing

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

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

frame-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-ancestors must be ignored in a CSP delivered via a <meta> element (not supported), as are report-uri and sandbox.

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

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

Do not add X-Frame-Options: ALLOW-FROM. Modern browsers ignore it and it offers no protection.

Test your site now →

Check your headers

See whether a site sends X-Frame-Options or CSP frame-ancestors, and what browsers will do with them.