Response header
< X-Frame-Options: DENYX-Frame-Options Header
Learn how X-Frame-Options prevents clickjacking attacks by controlling whether your site can be embedded in frames, iframes, or objects on other domains.
- Direction
- Response
- Category
- Security
- Spec
- RFC 7034
- Status
- Legacy: Superseded by CSP frame-ancestors
On this page
TL;DR:
X-Frame-Options: DENYblocks framing;SAMEORIGINpermits it only when every ancestor has the framed document’s origin. To allow named partner origins, use CSPframe-ancestors.ALLOW-FROMis obsolete.
What is X-Frame-Options?
This response header controls whether a document can appear inside a frame. It reduces clickjacking opportunities: an attacker cannot place the protected document beneath a deceptive interface if the browser refuses to display it there.
It controls embedding of this document, not the child frames this document loads. Use CSP frame-src for that separate decision.
How X-Frame-Options Works
The browser checks the framed response’s policy and the ancestor chain. SAMEORIGIN is not just a check of the immediate parent. A same-origin wrapper inside a cross-origin top-level document still fails. HTML defines the current processing rules.
This is a display restriction, not request authorization. Do not rely on it to stop requests reaching an endpoint or to prevent CSRF. It also does not control ordinary top-level popup windows.
Syntax
Choose one response field value:
X-Frame-Options: DENY
Or:
X-Frame-Options: SAMEORIGIN
Directives
DENY rejects framing even by the same origin. SAMEORIGIN checks origins, including scheme and port. ALLOW-FROM https://partner.example.com is obsolete; modern browsers that do not support it ignore the header, rather than enforcing that allowlist. See MDN’s directive reference.
https://app.example.com and http://app.example.com do not match for SAMEORIGIN. Neither do two otherwise identical hosts on different ports. A development iframe served on port 5173 cannot frame a SAMEORIGIN document served on port 3000.
Common Examples
Illustrative header excerpt for a document that may be framed only within its own origin:
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
The policies express the same restriction. Use DENY with frame-ancestors 'none' when no framing is required.
Real-World Scenarios
A dashboard that embeds another dashboard route can use SAMEORIGIN if the entire ancestor chain shares the origin. A partner-hosted widget needs a CSP origin allowlist instead; adding ALLOW-FROM will not make that arrangement work reliably.
In an Express app, scope a partner policy to the widget document while keeping account pages unframeable. This example sets only framing directives; merge them into any existing CSP:
app.use((_req, res, next) => {
res.set('X-Frame-Options', 'DENY')
res.set('Content-Security-Policy', "frame-ancestors 'none'")
next()
})
app.get('/widget', (_req, res) => {
res.set('X-Frame-Options', 'SAMEORIGIN')
res.set('Content-Security-Policy',
"frame-ancestors 'self' https://partner.example.com")
res.sendFile('widget.html', { root: '/srv/app/public' })
})
Create the widget file at that fixed path. An enforced CSP-capable browser can display it for the named partner; a legacy XFO-only browser rejects that partner. The route policy must run after the default setter. A later proxy that re-adds frame-ancestors 'none' will block the widget again.
Clickjacking Attack Examples
An attacker can position a transparent framed control over an unrelated visible button. The user interacts with the framed page without intending to. This does not require reading the framed document’s DOM, and cross-origin framing does not itself give the attacker permission to read CSRF tokens. The same-origin policy is a separate boundary.
A controlled demonstration can use an iframe over a visible decoy button:
<div style="position:relative;width:400px;height:300px">
<button style="position:absolute;top:100px;left:100px">View details</button>
<iframe src="https://app.example.com/framing-demo"
title="Controlled clickjacking demonstration"
style="position:absolute;inset:0;width:400px;height:300px;opacity:0.05;border:0">
</iframe>
</div>
Use a harmless page you control, not a production state-changing action. The demonstration depends on aligning a framed control with the decoy. DENY prevents displaying that document in the frame; it does not need to detect the transparent styling or the attacker’s intent.
Migration to Content-Security-Policy
For a document framed by your own app and one partner:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com
An enforced CSP containing frame-ancestors overrides X-Frame-Options in conforming browsers. A report-only policy does not replace enforcement. Older clients enforcing only SAMEORIGIN will still reject the partner; that fallback has a deliberate compatibility cost.
The CSP specification also requires all ancestors to match. default-src does not provide a fallback for an omitted frame-ancestors directive.
Use these combinations when moving an existing policy:
| Required framing | XFO fallback | Enforced CSP directive |
|---|---|---|
| None | DENY | frame-ancestors 'none' |
| Own origin | SAMEORIGIN | frame-ancestors 'self' |
| Own origin and a partner | SAMEORIGIN rejects partner in XFO-only clients | frame-ancestors 'self' https://partner.example.com |
CSP accepts multiple source expressions; XFO does not accept a comma-separated partner list. Keep the partner’s scheme and port correct, and check whether an intermediate portal also appears in the ancestor chain.
Getting X-Frame-Options right
Send it as an HTTP response header on the document. <meta http-equiv="X-Frame-Options"> has no effect. CSP frame-ancestors also cannot be delivered through a meta element.
Server Configuration Examples
In an existing nginx server block:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
For Apache with mod_headers loaded:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
Merge the CSP directive into an existing policy rather than overwriting unrelated directives. Consult the nginx inheritance rules and Apache header tables if the application or proxy also sets these fields.
With Helmet installed in an existing Express app:
const helmet = require('helmet')
app.use(helmet.xFrameOptions({ action: 'deny' }))
Helmet’s option uses lowercase deny or sameorigin in configuration and emits the corresponding header. Its standalone XFO middleware does not supply a partner CSP allowlist.
For a Caddy site that must never be framed:
app.example.com {
header {
X-Frame-Options DENY
Content-Security-Policy "frame-ancestors 'none'"
}
reverse_proxy localhost:3000
}
The header directive sets the response fields. If the application already owns CSP, add frame-ancestors there instead of replacing the policy at the proxy.
Testing
curl -sS -D - -o /dev/null https://app.example.com/account
Check the final document response, then try this element from a different origin you control:
<iframe src="https://app.example.com/account" title="Framing test"></iframe>
Use the browser Console to identify the enforcing policy. Curl checks delivered headers; it does not implement framing enforcement.
Test the same document from its own origin too. DENY should block that case; SAMEORIGIN can allow it when all ancestors match. To verify an ancestor-chain problem, embed a same-origin wrapper page inside a different-origin top-level page and let the wrapper frame the protected route.
Network may show a successful document response even though the frame remains blank. The browser’s display decision comes after fetching the response, so an access-log 200 does not prove the page was frameable. Check the actual framed URL after redirects, especially if an authentication page has a different policy.
Check MDN’s compatibility data for legacy targets rather than assuming support in every browser version. Test both same-origin and partner framing when changing a deployed policy.
Common Errors and Solutions
An iframe blocked despite SAMEORIGIN may have a cross-origin ancestor or a stricter CSP policy. A missing header may be caused by a location-specific nginx header configuration. Avoid multiple conflicting X-Frame-Options fields; emit one intentional value at the final response boundary.
For a partner widget that still fails, first look for another CSP header with a stricter frame-ancestors. Multiple enforced CSP policies all apply; adding a permissive second policy does not loosen the first. Remove or revise the restrictive directive in its owning layer rather than appending another allowlist.
Related Headers
Frequently asked questions
What is X-Frame-Options?
X-Frame-Options controls whether a browser may display the response document inside a frame. It reduces clickjacking opportunities but is not authentication or CSRF protection.
What values can X-Frame-Options have?
DENY blocks all framing. SAMEORIGIN requires every ancestor to share the document origin. ALLOW-FROM is obsolete and is ignored by modern browsers that do not support it.
Should I use X-Frame-Options or CSP frame-ancestors?
CSP frame-ancestors supports explicit origin allowlists and overrides X-Frame-Options when delivered in an enforced response policy. X-Frame-Options can provide a more restrictive legacy fallback.
How do I allow specific sites to frame my page?
Use a CSP frame-ancestors response directive listing the permitted origins. Every ancestor must match; include self if same-origin framing is also required.
Sources
Related
Access-Control-Allow-Credentials Header
Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.
Access-Control-Allow-Headers Header
Learn how Access-Control-Allow-Headers specifies which custom HTTP headers can be used during cross-origin requests in CORS preflight responses.
Access-Control-Allow-Methods Header
Learn how Access-Control-Allow-Methods specifies which HTTP methods are permitted for cross-origin requests in CORS preflight responses.
Access-Control-Allow-Origin Header: CORS Errors and Fixes
Access-Control-Allow-Origin explained: exact browser errors, why * fails with credentials, why you need Vary: Origin, and fixes for nginx, Express and Django.