X-Frame-Options vs CSP frame-ancestors: which should I use?
By StackSiren · Published
Use the Content-Security-Policy frame-ancestors directive as your modern clickjacking control, and keep sending X-Frame-Options as a simple fallback for older browsers. For most apps that should not be embedded, that means frame-ancestors 'none' plus X-Frame-Options: DENY (or 'self' / SAMEORIGIN if you embed your own pages).
Neither header encrypts traffic or stops XSS. They only answer: “who is allowed to put this page inside a frame?” Full header pack: security headers guide. Host recipes: Vercel, Netlify, HSTS explained, CSP for React + Vite.
What clickjacking is
Clickjacking (UI redressing) loads your page in a transparent or obscured iframe on an attacker's site, then tricks the user into clicking your real buttons — “Delete account”, “Confirm payment”, “Connect GitHub” — while they think they are clicking something else.
Defence is twofold:
- Don't allow unexpected parents to embed you — that is
X-Frame-Options/frame-ancestors. - For sensitive actions, require a fresh user gesture or re-auth so a single framed click is not enough.
This post is about the embedding headers. They are cheap, HTTP-only controls every small SaaS should set deliberately.
The two headers
X-Frame-Options
MDN documents three historical values; only two matter in modern browsers:
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
DENY— no embedding, not even by your own origin.SAMEORIGIN— only same-origin ancestors may embed the page.ALLOW-FROM uri— obsolete. Modern browsers that see it often ignore the whole header. Do not use it; useframe-ancestorsinstead.
Important: setting X-Frame-Options in a <meta http-equiv> tag has no effect. It must be an HTTP response header.
CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com;
'none'— similar intent toX-Frame-Options: DENY.'self'— similar intent toSAMEORIGIN.- Explicit origins — allow specific embedding parents (docs sites, partner portals, your marketing domain).
Notes from MDN that trip people up:
frame-ancestorsdoes not fall back todefault-src. A policy ofdefault-src 'none'still allows anyone to embed you unless you also setframe-ancestors.- It is a navigation directive and is not supported inside a
<meta>CSP — send it as an HTTP header. - It checks each ancestor. Nested iframes need every ancestor to match.
- It is different from
frame-src, which controls which URLs your page may load in its own iframes.
Which should you use?
| Goal | Recommended |
|---|---|
| App must never be framed | Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY |
| Only your own origin may frame you | frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN |
| A named partner may embed you | frame-ancestors 'self' https://partner.example.com (XFO alone cannot express this cleanly — skip obsolete ALLOW-FROM) |
| You actively want to be embeddable by anyone | Omit both (rare for dashboards; sometimes OK for public widgets) |
Why both? frame-ancestors is the flexible, modern control (CSP Level 2+, widely available since ~2018). X-Frame-Options remains useful for older clients and for scanners that still look for it specifically. When both are present, browsers that understand CSP enforce frame-ancestors; older ones fall back to XFO. Keep the values aligned so you do not accidentally allow in one what you deny in the other.
Setting them on common hosts
Confirm what you already send:
curl -sI https://yourapp.com/ | grep -iE 'x-frame-options|content-security-policy'
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{
"key": "Content-Security-Policy",
"value": "frame-ancestors 'none';"
}
]
}
]
}
If you already ship a fuller CSP (script-src, etc.), merge frame-ancestors into that single policy string rather than sending two competing Content-Security-Policy headers unless you understand how browsers combine them. Walkthrough: add security headers on Vercel and CSP for React + Vite.
Netlify / Bolt exports (_headers or netlify.toml)
# _headers
/*
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Details: Netlify security headers.
Next to HSTS and the rest
Clickjacking headers sit beside Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, and a careful CSP. HSTS alone does not stop framing: What is HSTS?. StackSiren's free first scan flags missing security headers on sites you own; the security headers checker is the focused tool for this cluster.
Embeds you actually want
If a partner must iframe your status page or a public widget:
- Prefer a dedicated embed route (
/embed/...) with a looserframe-ancestorsallowlist, and keep the main app on'none'/DENY. - List every allowed parent origin explicitly — no wildcards like
*inframe-ancestorsfor sensitive apps. - Strip cookies or use a cookieless embed session where possible so a framed page cannot act as the logged-in user without extra checks.
- Re-test with curl and a real iframe from the partner origin after every CSP edit.
For admin dashboards, billing, and anything with “delete” or “pay” buttons, default to no embedding.
How browsers pick a winner
When a response includes both headers:
- Browsers that implement CSP Level 2+ enforce
frame-ancestorsfor framing decisions. - Older clients that never learned
frame-ancestorsstill honourX-Frame-Options. - If you set contradictory values (for example XFO
DENYbutframe-ancestors https://evil.example), modern browsers follow CSP and older ones follow XFO — a messy split. Keep them aligned.
There is no benefit to omitting XFO “because CSP exists” on a public marketing site that still sees ancient WebViews. The header is two tokens long.
Rollout without locking yourself out
- Inventory every place you deliberately iframe yourself today (admin inside admin, status page, Storybook, payment widget docs).
- Ship
SAMEORIGIN/frame-ancestors 'self'first if you need same-origin frames; move toDENY/'none'once you confirm nothing breaks. - Use a preview deploy or staging hostname for the first CSP edit — a typo in a large CSP string can break scripts even when
frame-ancestorsis fine. - After production ships, re-run
curl -sIon apex andwww, and load the app inside a throwaway HTML file on another origin to confirm the frame is blocked.
<!-- save as /tmp/frame-test.html and open as a file or other origin -->
<iframe src="https://yourapp.com/" width="800" height="600"></iframe>
You should see an empty frame or a browser console error about framing, not your logged-in dashboard.
What these headers do not cover
- XSS — a script running on your origin can still do damage; that is CSP
script-src/ sanitisation, not framing. - CSRF — framing defence is not a CSRF token. State-changing POSTs still need SameSite cookies, tokens, or both.
- Open redirects / OAuth confusion — separate checks.
- API abuse — JSON endpoints are not “framed”; protect them with auth and rate limits.
Use framing headers as one line in the wider pack from the security headers guide, not as the whole security story.
Related reading
- Missing security headers guide
- Add security headers on Vercel
- Add security headers on Netlify
- Content-Security-Policy for React + Vite
- What is HSTS?
FAQ
Is X-Frame-Options enough on its own in 2026?
It blocks simple cases, but it cannot express an allowlist of third-party parents. Prefer frame-ancestors and keep XFO as a fallback.
Can I set these in a meta tag?
No for X-Frame-Options, and frame-ancestors is also not applied from a meta CSP. Use HTTP headers from your host or server.
What if DENY breaks my own docs site iframe?
Serve the embeddable page on a separate path with a tailored frame-ancestors list, or switch that path to 'self' / SAMEORIGIN only.
Does frame-ancestors stop someone loading my API in a script tag? No. It only controls embedding as a document frame. APIs still need auth, CORS allowlists and rate limits.
Will StackSiren see a missing frame-ancestors?
The free passive scan flags missing security headers broadly. After you deploy, re-check with curl -I and the security headers checker on a site you own.
Fix guides for this topic
Platform checklists: Vercel · Bolt