X-Frame-Options vs CSP frame-ancestors: Comparison and Migration
This guide sits under the Cross-Origin Frame Controls reference and answers a single question precisely: when do you use X-Frame-Options, when do you use the Content-Security-Policy frame-ancestors directive, and how do you migrate from one to the other without exposing yourself to clickjacking in between.
Direct answer: frame-ancestors is the modern, standards-track replacement for X-Frame-Options. It expresses everything XFO can plus an explicit origin allowlist XFO cannot. When both headers are present, every current browser enforces frame-ancestors and ignores X-Frame-Options. Deploy frame-ancestors as the real policy and keep X-Frame-Options: SAMEORIGIN only as a fallback for pre-2015 user agents.
Configuration Syntax & Exact Values
Map each XFO value to its frame-ancestors equivalent, then add origins XFO could never express:
X-Frame-Options: DENY → Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: SAMEORIGIN → Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: ALLOW-FROM x → Content-Security-Policy: frame-ancestors https://x.example
(no XFO equivalent) → Content-Security-Policy: frame-ancestors 'self' https://a.example https://b.example
Annotated directive breakdown:
'self'— the page’s own scheme + host + port may embed it. Quoted.'none'— no origin, including self, may embed it. Quoted; mutually exclusive with any source.https://a.example— an explicit host source. Not quoted. Add as many as needed, space-separated.- Path and query in a source are ignored; matching is at origin granularity.
Consolidate every CSP directive into a single Content-Security-Policy header. Multiple CSP headers are each enforced independently (logical AND), which produces a stricter policy than intended.
Server-Side Configuration
Nginx
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always;
add_header X-Frame-Options "SAMEORIGIN" always;
always emits both headers on 4xx/5xx responses; without it Nginx drops add_header on error pages, leaving them frameable.
Apache (mod_headers)
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner.example"
Header always set X-Frame-Options "SAMEORIGIN"
always overrides the default onsuccess table so error responses carry the headers too.
Cloudflare (Transform Rules)
Rules → Transform Rules → Modify Response Header → Set static:
Content-Security-Policy = frame-ancestors 'self' https://partner.example
X-Frame-Options = SAMEORIGIN
Use Set, not Add, so the edge replaces any origin-set header instead of duplicating it.
Node/Helmet
const helmet = require('helmet');
app.use(
helmet({
contentSecurityPolicy: {
directives: { frameAncestors: ["'self'", 'https://partner.example'] },
},
// frameguard still emits X-Frame-Options: SAMEORIGIN as the legacy fallback
})
);
Diagnostic & Verification Steps
curl -sI https://your-domain.com | grep -iE 'content-security-policy|x-frame-options'
Expected output on a correctly migrated page:
content-security-policy: frame-ancestors 'self' https://partner.example
x-frame-options: SAMEORIGIN
Confirm there is exactly one of each header — a count of 2 for either means origin and CDN are both setting it:
curl -sI https://your-domain.com | grep -ci x-frame-options # expect 1
Then load the page from an unlisted origin in a test iframe and open DevTools Console. A blocked load reports:
Refused to display 'https://your-domain.com/' in a frame because an ancestor
violates the following Content Security Policy directive: "frame-ancestors 'self' https://partner.example".
The message naming frame-ancestors (not X-Frame-Options) confirms the modern directive is the one being enforced.
Edge Cases, Security Implications & Safe Rollback
- Dual-header redundancy. Shipping both headers is safe but redundant on modern browsers —
frame-ancestorsalways wins there. The only reason to keepX-Frame-Optionsis measurable pre-2015 user-agent traffic; otherwise the second header is pure noise and one more thing to keep in sync. - Legacy UA fallback gap. IE11 and Safari before 10 ignore
frame-ancestorsentirely. If you dropX-Frame-Options, those clients lose all framing protection. KeepX-Frame-Options: SAMEORIGINuntil analytics confirm negligible legacy share, then remove it. - Policy divergence. If
frame-ancestorsis more permissive than your XFO value, modern browsers enforce the permissive one and legacy browsers enforce the stricter one — inconsistent protection across your user base. Keep the two values aligned in meaning. sandboxdoes not substitute. An iframe’ssandboxattribute restricts what the embedded page can do; it does not control who may embed you.frame-ancestorsandsandboxapply independently.- Safe rollback. If
frame-ancestorsbreaks a legitimate embed and you cannot immediately add the missing origin, fall back to XFO-only rather than removing all framing control:
Header unset Content-Security-Policy
Header always set X-Frame-Options "SAMEORIGIN"
Never roll back to no framing header. For pinpointing which embed broke, see fixing X-Frame-Options blocking legitimate iframes.
Frequently Asked Questions
If frame-ancestors always wins, why keep X-Frame-Options at all?
Solely to protect pre-2015 browsers that never implemented frame-ancestors. On every modern engine the CSP directive is authoritative and XFO is ignored. Once your legacy traffic is negligible, drop XFO.
Can I migrate without a report-only phase?
You can, but a Content-Security-Policy-Report-Only window of 14-30 days surfaces legitimate embeds you forgot to allowlist before they break in production. It is the lower-risk path for any page that is embedded by partners.
Does combining DENY and frame-ancestors ‘self’ block everything?
No. The frame-ancestors 'self' directive wins on modern browsers, so the page remains embeddable on its own origin despite the DENY. Align the two values or the result surprises you.
Conclusion
Roll out incrementally: set frame-ancestors in report-only mode on staging, review violation reports to confirm no legitimate embed is caught, promote to an enforcing Content-Security-Policy in production, and keep X-Frame-Options: SAMEORIGIN as a legacy fallback until traffic analytics justify removing it. The migration is low-risk when each origin you legitimately allow is verified before enforcement.