What is X-Frame-Options?
X-Frame-Options is an HTTP response header that tells the browser whether a page can load inside a <frame>, <iframe>, <embed>, or <object>. It has two working values: DENY blocks all framing, and SAMEORIGIN allows framing only by pages from the same origin. Its main job is to stop clickjacking.
The header was standardized in RFC 7034 in 2013. Every major browser still enforces it. Today it works alongside the Content Security Policy (CSP) frame-ancestors directive, which is the modern replacement for most use cases.
A typical response looks like this:
HTTP/1.1 200 OK
Content-Type: text/html
X-Frame-Options: SAMEORIGIN
The server sends the header. The browser enforces it. If a page is framed against the rule, the browser shows a blank frame and logs an error in the console.
How X-Frame-Options Work?
The header takes one value per response. Browsers recognize DENY and SAMEORIGIN. ALLOW-FROM is obsolete and ignored by current browsers.
| Value | What it allows | Browser support | Use it? |
|---|---|---|---|
DENY |
No framing at all, including by your own site | All browsers | Yes, for pages that never need to be framed |
SAMEORIGIN |
Framing only by pages on the same origin (scheme, host, and port) | All browsers | Yes, when your own app frames its pages |
ALLOW-FROM uri |
Framing by one named origin | Not supported in Chrome, Edge, Safari, or current Firefox | No. Use CSP frame-ancestors |
DENY
X-Frame-Options: DENY stops the page from loading in any frame, from any origin. This includes frames on your own domain. Use it for login pages, account settings, payment steps, and admin panels.
X-Frame-Options: DENY
SAMEORIGIN
X-Frame-Options: SAMEORIGIN lets the page load in a frame only when the framing page has the same origin. Origin means the same scheme, host, and port. https://app.example.com and https://www.example.com are different origins.
X-Frame-Options: SAMEORIGIN
Modern browsers check every ancestor in the frame chain. If any parent frame comes from a different origin, the browser blocks the page.
ALLOW-FROM (obsolete)
ALLOW-FROM was meant to allow one trusted origin.
X-Frame-Options: ALLOW-FROM https://trusted.example.com
Chrome and Safari never supported it. Firefox removed support in version 70. A browser that does not understand ALLOW-FROM ignores the whole header. The page then has no framing protection at all. Use Content-Security-Policy: frame-ancestors instead (covered below).
You may also see ALLOWALL in old configs. It is not a standard value. It turns protection off. If a page must be embeddable, remove the header and control framing with frame-ancestors.
X-Frame-Options DENY vs SAMEORIGIN: Which Should You Use?
Use DENY by default. Switch to SAMEORIGIN only for pages your own application loads in a frame.
| Scenario | Recommended value |
|---|---|
| Login, signup, password reset | DENY |
| Checkout and payment pages | DENY |
| Admin panels and account settings | DENY |
| Dashboards that embed your own reports or widgets | SAMEORIGIN |
| CMS editors with live preview frames (for example, WordPress admin) | SAMEORIGIN |
| Pages partners or customers must embed on their sites | Neither. Use CSP frame-ancestors with an allowlist |
A simple test helps. Ask whether any page on your own origin frames this page. If no, use DENY. If yes, use SAMEORIGIN. If another domain needs to frame it, X-Frame-Options cannot express that rule.
Is X-Frame-Options Deprecated?
No. The header itself is still supported and enforced by every major browser. Only the ALLOW-FROM value is obsolete. CSP frame-ancestors is now the preferred control, and the OWASP Clickjacking Defense Cheat Sheet recommends sending both.
X-Frame-Options vs CSP frame-ancestors
| X-Frame-Options | CSP frame-ancestors |
|
|---|---|---|
| Block all framing | DENY |
frame-ancestors 'none' |
| Allow same origin only | SAMEORIGIN |
frame-ancestors 'self' |
| Allow specific external domains | Not possible | frame-ancestors 'self' https://partner.com |
| Allow multiple domains | Not possible | Yes, space-separated list |
| Wildcard subdomains | No | Yes, https://*.example.com |
Works in a <meta> tag |
No | No |
| Precedence when both are set | Ignored by modern browsers | Wins |
When a response carries both headers, browsers that support frame-ancestors apply it and ignore X-Frame-Options. Older browsers fall back to X-Frame-Options. Sending both gives full coverage. Keep the two policies consistent so behavior does not change by browser.
How to allow multiple domains
X-Frame-Options allows one value and no domain list. To allow framing from several sites, use CSP:
Content-Security-Policy: frame-ancestors 'self' https://partner-one.com https://partner-two.com
Pair it with X-Frame-Options: SAMEORIGIN for legacy browsers. Modern browsers follow the CSP list. Legacy browsers fall back to same-origin only.
What Threats Does X-Frame-Options Prevent?
X-Frame-Options prevents attacks that depend on loading your page inside an attacker-controlled frame. The main one is clickjacking.
Clickjacking – The attacker loads your page in a transparent iframe and places a decoy button on top. The user thinks they are clicking the decoy. They actually click a button on your page while logged in. This can change account settings, approve a payment, grant OAuth access, or enable a camera permission. With DENY or SAMEORIGIN, the browser refuses to render your page in the attacker’s frame, so there is nothing to click.
UI redress variants – These include cursorjacking, likejacking, and drag-and-drop attacks that steal text or tokens from a framed page. Each needs the target page inside a frame. The header removes that precondition.
Frame-based phishing – An attacker can wrap your real login page inside a lookalike site and surround it with fake prompts. Blocking framing forces users to see your page in their own browser tab, with your real URL in the address bar.
X-Frame-Options does not stop cross-site scripting, CSRF, or a phishing site that copies your page HTML. It only controls framing. Treat it as one layer in a set of security headers.
How to Set the X-Frame-Options Header
Set the header at the web server or in the application framework. Use one place only, so the header is not sent twice. Each example below uses SAMEORIGIN. Replace it with DENY where that fits.
Apache
Enable mod_headers, then add this to the virtual host or .htaccess:
Header always set X-Frame-Options "SAMEORIGIN"
The always keyword adds the header to error responses as well.
Nginx
Add this to the server block:
add_header X-Frame-Options "SAMEORIGIN" always;
Nginx does not inherit add_header into a location block that defines its own add_header. Repeat security headers in those blocks, or use a shared include file.
IIS
Add this to web.config:
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
</system.webServer>
Node.js (Express with Helmet)
const helmet = require("helmet");
app.use(helmet.frameguard({ action: "sameorigin" }));
Django
Django includes XFrameOptionsMiddleware and sends DENY by default. To change it, set this in settings.py:
X_FRAME_OPTIONS = "SAMEORIGIN"
Spring Security
Spring Security sends DENY by default. To allow same-origin framing:
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
Adding CSP frame-ancestors
Send frame-ancestors next to X-Frame-Options with the same intent. Apache example:
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
If you already send a CSP header, add frame-ancestors to that policy. Two separate CSP headers are both enforced, which can block more than you expect.
Neither header works in an HTML <meta http-equiv> tag. Browsers ignore both there. They must be sent as HTTP response headers.
How to Test X-Frame-Options
Check the header three ways: from the command line, in the browser, and with a test frame.
- Command line – Run
curl -sI https://www.example.com | grep -i x-frame-options. The output should show one line with the value you expect. - Browser DevTools – Open the Network tab, reload the page, select the main document, and look under Response Headers. Check for duplicate X-Frame-Options lines and for a
frame-ancestorsdirective in the CSP header. - Test frame – Save the snippet below as an HTML file on a different origin and open it. A protected page shows a blank frame and a console error.
<iframe src="https://www.example.com/login" width="800" height="600"></iframe>
Test error pages, redirects, and API responses that return HTML. Those responses often skip headers that the main pages carry.
Fixing “Refused to Display in a Frame” Errors
This console error means the framed page sent X-Frame-Options or frame-ancestors, and your page is not allowed. Chrome shows it like this:
Refused to display 'https://www.example.com/' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
The fix depends on who owns the framed page.
You own the framed page. Decide which sites need to embed it. For your own origin, use SAMEORIGIN or frame-ancestors 'self'. For external sites, remove X-Frame-Options from that route and send frame-ancestors with an explicit allowlist. Never remove protection site-wide to fix one embed.
A third party owns the framed page. Sites such as apple.com, whatsapp.com, and most banks block framing on purpose. You cannot override the header from your side. Proxying their page to strip the header breaks their security model and often their terms of service. Use their official embed code or API, or open the page in a new tab with a link.
The header appears twice. A web server and an application framework can both add it. Conflicting values, such as DENY and SAMEORIGIN, can be treated as invalid or handled differently by each browser. Send the header from one layer only.
Best Practices and Common Mistakes
Most X-Frame-Options problems come from a missing header on some responses or a conflict between layers.
Best practices
- Default to
DENY. Relax it toSAMEORIGINonly on routes your app frames. - Send CSP
frame-ancestorswith the same intent for modern browsers. - Set the header on every HTML response, including error pages and redirects.
- Set it in one layer: web server, CDN, WAF, or application. Pick one.
- Re-test after framework upgrades, CDN changes, or new reverse proxies.
- Combine it with other security headers such as HSTS,
X-Content-Type-Options, and a full CSP.
Common mistakes
- Using
ALLOW-FROM. Current browsers ignore it, and the page ends up unprotected. - Setting the header in a
<meta>tag. Browsers ignore it there. - Sending
DENYfrom the server andSAMEORIGINfrom the app in the same response. - Relying on JavaScript frame-busting code alone. Attackers can bypass it with the iframe
sandboxattribute. - Removing the header site-wide to make one embed work.