Remediation guide

X-Frame-Options vs CSP frame-ancestors: Which Do You Need?

Security scanners often flag two separate clickjacking findings on the same site: a missing X-Frame-Options header, and a Content-Security-Policy header without a frame-ancestors directive. They protect against the same attack — so which one do you actually need to set? This guide explains what each one does, how browser precedence works when both are present, and the exact snippets to deploy on every major platform.

X-Frame-Options: ALLOW-FROM is dead. Chrome, Edge, and Safari never supported it, and Firefox dropped it in version 70 (2019). If you need to allow framing by a specific third-party domain, use CSP frame-ancestors with that origin instead.

What X-Frame-Options does

The X-Frame-Options (XFO) response header, first shipped in browsers around 2009, prevents your page from being loaded inside a <frame>, <iframe>, or similar embedding element on a different site. The attack it guards against is clickjacking: an attacker loads your page invisibly in an iframe, lays their own UI on top, and tricks users into clicking buttons they can’t see — confirming a payment, deleting an account, changing a password.

The header has exactly two useful values in 2026:

ValueBehaviourUse when
DENY The page cannot be framed by anyone — not even by a page on the same origin. No legitimate embedding use case exists. The safest default for most sites.
SAMEORIGIN The page can be framed by pages on the same origin; all other origins are blocked. You intentionally embed your own pages in iframes (e.g., a widget on your own domain).
ALLOW-FROM uri Intended to allow framing by a specific third-party origin. Avoid. Unsupported in Chrome, Edge, and Safari; removed from Firefox in 2019. Use CSP frame-ancestors instead.

The header for most sites is simply:

X-Frame-Options: DENY

What CSP frame-ancestors does

The frame-ancestors directive is part of Content Security Policy Level 2 (2015). It controls which origins can embed your page in a <frame>, <iframe>, <embed>, or <object> element. It supersedes X-Frame-Options in all browsers that support CSP, and it provides expressive power that XFO never had.

Common values:

# Block all framing (equivalent to XFO: DENY)
Content-Security-Policy: frame-ancestors 'none'

# Allow same-origin only (equivalent to XFO: SAMEORIGIN)
Content-Security-Policy: frame-ancestors 'self'

# Allow same-origin plus one trusted partner
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com

# Allow any subdomain of your own domain
Content-Security-Policy: frame-ancestors 'self' https://*.yoursite.com
frame-ancestors goes in the HTTP response header, not a meta tag. The CSP spec explicitly prohibits frame-ancestors in <meta http-equiv> tags — it is only enforced when delivered as a response header. Browsers silently ignore it in a meta tag.

The directive is typically included alongside your other CSP rules:

Content-Security-Policy: default-src 'self'; script-src 'self'; frame-ancestors 'none'

Key differences at a glance

FeatureX-Frame-OptionsCSP frame-ancestors
Browser support All browsers including IE 8+ All browsers except IE (all versions)
Block all framing DENY 'none'
Allow same-origin only SAMEORIGIN 'self'
Allow a specific third-party origin Not possible (ALLOW-FROM unsupported) Yes — list any number of specific origins
Allow wildcard subdomains No Yes — https://*.yoursite.com
Covers <embed> and <object> Inconsistent across browsers Yes, by spec
Delivered via meta tag No effect Silently ignored (header only)

Precedence: what happens when both headers are present

The CSP specification is clear: when a browser supports CSP Level 2 and a response contains both X-Frame-Options and a Content-Security-Policy: frame-ancestors directive, the browser uses frame-ancestors and ignores X-Frame-Options. This is the behaviour in Chrome, Firefox, Edge, and Safari.

Internet Explorer does not support CSP at all. When IE receives both headers, it reads X-Frame-Options and ignores the entire Content-Security-Policy header. This is the sole remaining reason to set X-Frame-Options — it is the only clickjacking protection IE recognises.

BrowserWhich header it uses
Chrome, Edge (Chromium)frame-ancestors (ignores XFO when CSP is present)
Firefoxframe-ancestors (ignores XFO when CSP is present)
Safariframe-ancestors (ignores XFO when CSP is present)
Internet Explorer 9–11X-Frame-Options (ignores entire CSP header)

Crucially, setting both headers with consistent values — DENY paired with 'none', or SAMEORIGIN paired with 'self' — creates no conflict. Modern browsers use frame-ancestors; IE falls back to XFO. The two headers target different browser generations and complement each other cleanly.

What to set in 2026

The recommended approach is belt-and-suspenders: include frame-ancestors in your CSP, and also send X-Frame-Options. They do not conflict, and together they cover every browser from IE 9 to Chrome latest.

For a site with no legitimate embedding use case (the vast majority):

X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'; ...; frame-ancestors 'none'

For a site that embeds its own pages in iframes:

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: default-src 'self'; ...; frame-ancestors 'self'

For a site that allows embedding by a trusted partner (XFO alone cannot express this):

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: default-src 'self'; ...; frame-ancestors 'self' https://dashboard.partner.com

In this third case, modern browsers use frame-ancestors and allow the partner. IE uses SAMEORIGIN and blocks the partner — but IE has less than 1% global usage as of 2026, so this is an acceptable tradeoff. IE also cannot render your modern JavaScript application, making this moot in practice.

If you have no CSP yet. You can add X-Frame-Options: DENY as a standalone header today — it is a one-liner and immediately eliminates clickjacking exposure in all browsers. Then build out a full CSP (start in report-only mode as described in the CSP guide) and add frame-ancestors 'none' once your policy is stable.

Add the headers on your platform

The snippets below add both headers. If you already have a Content-Security-Policy header, append ; frame-ancestors 'none' to the existing value instead of duplicating the header line.

nginx

add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;

Apache (.htaccess)

<IfModule mod_headers.c>
  Header always set X-Frame-Options "DENY"
  Header always append Content-Security-Policy "frame-ancestors 'none'"
</IfModule>

Vercel (vercel.json)

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" }
      ]
    }
  ]
}

Next.js (next.config.js)

const securityHeaders = [
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" }
];

module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

Netlify (_headers file)

/*
  X-Frame-Options: DENY
  Content-Security-Policy: frame-ancestors 'none'

Cloudflare Pages (_headers file)

/*
  X-Frame-Options: DENY
  Content-Security-Policy: frame-ancestors 'none'
GitHub Pages note. GitHub Pages does not support custom response headers via a configuration file. If your site is hosted on plain GitHub Pages without a CDN in front, you cannot set frame-ancestors as a response header. In this case, X-Frame-Options is also unavailable. The options are: (1) put Cloudflare in front of your GitHub Pages site and add the headers via a Cloudflare Transform Rule, or (2) accept the gap and note it for the next platform migration. HardenCheck’s paste mode lets you verify what headers are actually being sent.

Verify with HardenCheck

Paste your raw HTTP response headers into HardenCheck after deploying. It reads both X-Frame-Options and the frame-ancestors directive inside your Content-Security-Policy. If your CSP has a valid frame-ancestors value, HardenCheck treats your framing protection as satisfied even if X-Frame-Options is absent, since modern browsers will use frame-ancestors. The paste mode — using output from curl -I https://your-site.com — is the most accurate way to confirm what your server is actually sending.

Grade my headers with HardenCheck →

Frequently asked questions

Does X-Frame-Options still work in modern browsers?

Yes — Chrome, Firefox, Edge, and Safari all still respect X-Frame-Options. But when a Content-Security-Policy with a frame-ancestors directive is also present in the same response, modern browsers use frame-ancestors and ignore X-Frame-Options. XFO still works as a fallback for browsers that don’t support CSP (primarily Internet Explorer), but frame-ancestors is the primary enforcement mechanism in every current browser.

Can I use X-Frame-Options: ALLOW-FROM to whitelist a specific embedding site?

No, not reliably. ALLOW-FROM was removed from the spec and is unsupported in Chrome, Edge, and Safari — those browsers treat any ALLOW-FROM value the same as DENY. Firefox removed ALLOW-FROM support in version 70 (2019). The modern equivalent is CSP frame-ancestors with a specific origin: Content-Security-Policy: frame-ancestors 'self' https://partner.example.com. That works in all CSP2-supporting browsers.

What does frame-ancestors protect that X-Frame-Options doesn’t?

Two things. First, frame-ancestors covers <embed> and <object> elements in addition to <frame> and <iframe>X-Frame-Options behaviour for embed and object tags is inconsistently implemented across browsers. Second, frame-ancestors supports multiple allowed origins and wildcard matching (https://*.yoursite.com), which X-Frame-Options cannot express at all since ALLOW-FROM only ever accepted a single origin and is now deprecated.

Should I set both headers in the same response?

Yes, the belt-and-suspenders approach is recommended. Set X-Frame-Options: DENY (or SAMEORIGIN) and include frame-ancestors 'none' (or 'self') in your CSP. Modern browsers use frame-ancestors and ignore XFO when both are present, so there is no conflict. Internet Explorer and other browsers without CSP support fall back to X-Frame-Options. The combination provides full coverage across every browser generation.

Does HardenCheck check both X-Frame-Options and frame-ancestors?

Yes. HardenCheck reads X-Frame-Options directly and also parses your Content-Security-Policy header for a frame-ancestors directive. If your CSP has a valid frame-ancestors value, HardenCheck treats your framing protection as satisfied even if X-Frame-Options is absent — because modern browsers use frame-ancestors. If neither is present, you will see a clickjacking warning in the report.

Does a <meta http-equiv> CSP tag cover frame-ancestors?

No. The CSP specification explicitly prohibits frame-ancestors in <meta http-equiv> tags — it is only enforced when delivered as an HTTP response header. Browsers silently ignore a frame-ancestors directive inside a meta tag. If your platform doesn’t let you set response headers (such as plain GitHub Pages), frame-ancestors will not work, and you must rely on X-Frame-Options instead, which some CDN layers do allow via transform rules.

Also in the Copper Bay Labs ship-safety suite

Framing protection is one layer — these tools and guides cover the rest: