X-Frame-Options Explained: Stopping Clickjacking
Part of our guide to HTTP Security Headers Explained: The Complete Guide.
X-Frame-Options tells a browser whether your page is allowed to load inside a frame, iframe, embed, or object element. Set it to a restrictive value and you shut down clickjacking, an attack that hides your real page behind a decoy and steals the clicks users think they are giving to something else. The header is simple, has only two values worth using, and belongs on almost every page you serve. For the wider picture of how this fits with the rest of your response headers, see our pillar guide on HTTP security headers explained.
Clickjacking works by deception. An attacker loads your legitimate page (say, a bank transfer form or a "delete account" button) into a transparent frame and stacks it on top of a page they control. The victim sees a harmless looking button or game, clicks it, and the click actually lands on your framed page while they are logged in. X-Frame-Options closes that door by refusing to render in a frame at all. The header is defined in RFC 7034, which standardized behavior that browsers had already shipped for years. If you are auditing a whole site rather than one header, our companion guide on HTTP security headers explained covers the full set.
The values
There are three documented values. Only two are safe to use today.
| Value | Behavior | Use it? |
|---|---|---|
DENY |
No page may frame this resource, including pages on the same origin. | Yes, for pages that should never be embedded anywhere. |
SAMEORIGIN |
Only a top-level page from the same origin may frame this resource. | Yes, when your own site needs to embed the page. |
ALLOW-FROM uri |
Permit framing only by the listed origin. | No. Deprecated and inconsistently supported. |
ALLOW-FROM was never reliable. Several browsers ignored it, and Chrome never implemented it, so a page that depended on it could end up either fully blocked or fully open depending on the visitor. If you need to allow specific external origins to frame your content, that is precisely the job CSP handles.
A typical header looks like this:
X-Frame-Options: DENY
or, for a page your own app frames:
X-Frame-Options: SAMEORIGIN
Why frame-ancestors is the modern replacement
The Content Security Policy frame-ancestors directive does everything X-Frame-Options does and more. Where the older header gives you one fixed origin at best, frame-ancestors accepts a list of source expressions, supports wildcards, and lets you mix scheme and host rules. It is specified in the W3C CSP Level 3 document, and when a browser supports CSP it uses frame-ancestors and ignores X-Frame-Options entirely.
The mapping is direct:
| X-Frame-Options | CSP frame-ancestors equivalent |
|---|---|
DENY |
frame-ancestors 'none' |
SAMEORIGIN |
frame-ancestors 'self' |
ALLOW-FROM https://a.example |
frame-ancestors https://a.example |
So a page that should never be framed can carry:
Content-Security-Policy: frame-ancestors 'none'
and one that may be embedded by your own site plus a partner can carry:
Content-Security-Policy: frame-ancestors 'self' https://partner.example
That second case is the one X-Frame-Options simply cannot express well, which is the main reason to move.
Set both
Here is the practical recommendation: send both headers. Modern browsers obey frame-ancestors, so you get its flexibility there. Older or unusual clients that predate CSP support fall back to X-Frame-Options, so you keep protection for them. The two do not fight, because anything that reads CSP ignores the legacy header by spec.
A page locked down everywhere would carry:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
A few things to keep in mind while you roll this out:
- Set the header at the response level for every HTML route, not just the login page. Attackers will target whichever page has state-changing actions.
- X-Frame-Options is a response header only. A
metatag version does nothing, because the browser has to decide whether to frame before it parses the document body. - If parts of your app legitimately need to be embedded (a checkout widget, a help bubble), scope
frame-ancestorsto those specific routes rather than loosening the whole domain. - Test after deploying. A single misconfigured proxy or CDN rule can strip the header before it reaches the user.
Once both headers are in place, confirm they actually arrive on the wire. Our walkthrough on how to check security headers shows how to read them from a live response, and the CSP explained guide goes deeper on frame-ancestors and the rest of the policy.
Want to see what your site sends right now? Scan your domain's headers free at domainintel.app and get a clear report of what is set, what is missing, and what to fix.
Frequently asked questions
What does X-Frame-Options do?
It controls whether a browser may render your page inside a frame, iframe, embed, or object element. Blocking that framing prevents clickjacking, where an attacker layers your page invisibly over their own to trick users into clicking it.
What is the difference between DENY and SAMEORIGIN?
DENY blocks all framing of the page, even by pages on your own domain. SAMEORIGIN allows framing only when the top-level page comes from the same origin as the framed page, so your own site can still embed it but third parties cannot.
Should I use X-Frame-Options or CSP frame-ancestors?
Use CSP frame-ancestors as the modern, more flexible control, since it supports multiple allowed origins and wildcards. Send X-Frame-Options too for coverage on older browsers that ignore CSP. When both are present, modern browsers honor frame-ancestors.