Checked on 2026-08-24.
Put a website inside another website and one of two things happens: it loads, or you get an empty rectangle and a line in the console about a refused frame. The rectangle is almost always the work of a header the site sent back with its page, and since 2009 the header doing that work has been X-Frame-Options.
It's a strange piece of the web platform: invented by browser vendors before anyone wrote it down, documented years after it shipped, given a third value that never worked properly, and then replaced by something better while everyone kept sending it anyway. It's still worth understanding, because most of the internet still sends it.
Where it came from
RFC 7034, the informational document that finally wrote the header down, opens with the timeline: "In 2009 and 2010, many browser vendors ... introduced the use of a non-standard HTTP header field 'X-Frame-Options' to protect against clickjacking." The earliest source it cites is a Microsoft post from January 2009, "IE8 Security Part VII: Clickjacking Defenses," and the first entry in its appendix of supporting browsers is Internet Explorer 8. The usual order was reversed: browsers shipped it, sites used it, and the RFC arrived in October 2013 to describe what had already happened.
The attack it was built against is clickjacking: an attacker loads a real site in an invisible frame, positions it over a decoy button, and collects your click on the real site's controls while you think you're clicking something else. A site that refuses to be framed removes the invisible layer, and the attack has nothing left to sit under. We go through the attack itself in clickjacking explained.
DENY and SAMEORIGIN
Today the header has exactly two valid values. The WHATWG HTML Standard, which now carries the processing model that browsers actually implement, states the grammar in one line:
X-Frame-Options = "DENY" / "SAMEORIGIN"
DENY means no frame, from anywhere, including the site's own pages. A site that sends DENY cannot embed itself in itself.
SAMEORIGIN means the framing has to stay inside one origin. An origin is the scheme, the host, and the port taken together, so https://example.com and http://example.com are two different origins, and so are example.com and www.example.com. That trips people up more often than the header itself does.
The interesting detail is which frames get checked. The current algorithm walks the whole chain: it starts at the framed page's container document and keeps climbing, and if any document along the way is a different origin, the load is refused. That matters for nested frames, where your page is inside someone else's page.
This is one of the places where the header's history shows. RFC 7034 flagged the ambiguity in 2013: the implementations it knew about compared the framed page against the top-level browsing context, while other implementations, it allowed, might compare against the framing page instead, or against "the whole chain of nested frames in between." The modern specification settles the question in favour of the whole chain. If you are reading old advice about SAMEORIGIN and nesting, check its date, because the ground moved under it.
ALLOW-FROM, and why it's gone
The RFC's grammar had a third value, ALLOW-FROM followed by a single origin, meant to name one site allowed to do the framing. It reads like the obvious missing feature, and it never became something you could rely on.
RFC 7034 was blunt about this in its own text: "'ALLOW-FROM' was initially an Internet Explorer extension and, at the time of writing, has not been uniformly implemented by other user agents." That's the honest version of the history, and it's the one worth repeating, because claims about exactly which browsers supported it in which versions are easy to get wrong. What's certain is that support was never uniform, and that the current HTML Standard closes the door completely: the ALLOW-FROM variant, it says, "is not to be implemented."
Here's the part that makes this more than trivia. A browser that meets a value it doesn't recognise does not fall back to blocking. It treats the header as though it were never sent. The specification spells this out with a table of non-conforming values, and the row for ALLOW-FROM=https://example.com/ gives the result as "embedding allowed (from anywhere)."
Read that twice if you maintain something old. A site still sending ALLOW-FROM today has no clickjacking protection from this header at all, while its configuration file looks like it does. The header is not a warning system. It fails open.
There's one exception to the fail-open rule: a response carrying more than one value gets stricter treatment. If the set of values contains any of deny, allowall, or sameorigin, the frame is blocked — even when the value sitting next to it is meaningless. SAMEORIGIN, DENY blocks, and so does SAMEORIGIN, INVALID. A set holding none of those three is allowed through, which is why the spec's table gives INVALID, INVALID as permitted. The note explains the reasoning: the goal is "to block any attempts at applying X-Frame-Options which were trying to do something valid, but appear confused."
The header belongs to the framed page
This is the point most people get backwards, so it's worth stating plainly: X-Frame-Options is sent by the page going into the frame, not by the page that holds the frame.
If you're building a dashboard, a documentation portal, or a demo that pulls in a third-party site, you have no say in this. The header travels with someone else's response. Nothing in your HTML changes it, no attribute on your <iframe> element overrides it, and no amount of CSS makes the refusal go away. Your page is the party being told no.
The mirror image is also true. The check in the specification begins by asking whether the document is being loaded into a child frame at all, and returns early if it isn't. Your own site's X-Frame-Options has no effect on what your site is allowed to frame. It only governs where your site may be framed. Two different questions, one header, and a lot of wasted debugging in between.
It's a response header, and only a response header
You cannot set it with a <meta> tag. MDN says so in a warning box: setting X-Frame-Options inside a <meta> element "has no effect. X-Frame-Options is only enforced via HTTP headers."
The reason is timing. The browser decides whether the frame is allowed when the response arrives, before it has parsed a single tag of the document. A <meta> element is inside the document, so by the time the browser reads it the decision is already made and the page is already loading in the frame it was meant to prevent.
If your platform lets you edit HTML but not response headers, this header simply isn't available to you. That's a real constraint on some static hosts, and the answer is a host or a proxy that can add headers, not a better meta tag.
What it looks like on real sites
We fetched 60 popular web apps on 2026-08-24 and recorded the framing headers each one returned. Seven allowed a frame, 53 blocked it. Here are five of the rows:
| Site |
X-Frame-Options |
frame-ancestors |
| Discord |
DENY |
— |
| YouTube |
SAMEORIGIN |
— |
| GitHub |
deny |
'none' |
| Trello |
— |
— |
| Wikipedia |
— |
— |
Three things in that small table are worth pointing out. GitHub's value is lowercase, because the comparison is case-insensitive. GitHub also sends both headers, which is belt and braces: the modern directive for modern browsers, the legacy header for anything that predates it. And Trello and Wikipedia send neither header, which is why a page can put them in a frame — a permission granted by silence rather than by an explicit yes.
The full table, every row with its raw value and the caveats about what a header read can and cannot prove, is at can I embed this site in an iframe?.
What replaced it
The replacement is the frame-ancestors directive in a Content Security Policy, and it doesn't merely sit alongside the old header — it overrides it. The CSP specification says so directly, and the HTML Standard's algorithm implements it: if the response carries an enforced policy containing frame-ancestors, the X-Frame-Options header is ignored entirely.
That's why sending both is safe rather than contradictory. Old browsers read the header they understand; current browsers read the directive and drop the header on the floor. It also means that when the two disagree, the one you can see in your curl output may not be the one deciding anything. We measured Evernote sending X-Frame-Options: DENY alongside a frame-ancestors list naming three specific origins; the list is the one a current browser obeys.
The frame-ancestors directive covers the syntax, the precedence rule, and the four ways the source list goes wrong.
Checking a site yourself
One request tells you what a site sends:
curl -sIL https://example.com | grep -i "x-frame-options\|content-security-policy"
Follow the redirects, and read the headers on every hop, not just the first. A site that bounces you to a login page can hand you the login page's headers while you believe you're looking at the app's. That is not a hypothetical: it's exactly what happens with Gmail, and we take it apart in why Gmail and Google Calendar never load in an iframe.
Questions
Can I turn off X-Frame-Options for a site I don't own?
Not on the site. The header is part of the site's own response, so changing it for everyone would mean changing the site. A browser extension can rewrite the header locally, inside your own browser, for frames you specifically allow, and that's how tools that embed reluctant sites do it. It changes nothing for anyone else, and it fails on sites whose protection runs deeper than the header — see why some sites won't load in a dashboard for where that line falls.
Does X-Frame-Options stop my page from framing other pages?
No. The header only applies to a document being loaded into a frame, and the browser's check returns early when the document isn't in one. Setting it on your own site controls where your site may be embedded. It has no effect on what your site is allowed to embed, which is governed by the other site's headers and, if you want to restrict it yourself, by the frame-src directive in your own Content Security Policy.
Is SAMEORIGIN enough for my site?
It's a reasonable default, and it's stricter than it looks: the browser checks every ancestor document in the chain, not just the outermost one, so a same-origin page nested inside a third-party page still fails the test. What SAMEORIGIN cannot do is name a partner site you want to allow, because the value that was supposed to do that never worked. If you need a guest list, you need frame-ancestors, and if you need to support browsers old enough to lack it, send SAMEORIGIN as well and accept that it will be ignored wherever the directive is understood.