DEV Community

Cover image for X-Frame-Options: How It Works and How to Set It | FortifyNet
Håkan Fägnell
Håkan Fägnell

Posted on Originally published at fortifynet.com

X-Frame-Options: How It Works and How to Set It | FortifyNet

X-Frame-Options: What It Does and How to Configure It Right

X-Frame-Options is an HTTP response header that tells the browser whether your page may be rendered inside a frame or iframe on another site. It has exactly two valid values: DENY (no framing at all) and SAMEORIGIN (framing allowed only from your own origin). Its purpose is to stop clickjacking, an attack where your site is loaded invisibly inside an attacker's page and your visitors are tricked into clicking things they cannot see. The modern replacement is the Content-Security-Policy directive frame-ancestors, and the 2026 best practice is to send both headers. This guide covers the correct setup for every major server, plus the mistakes that silently disable the protection.

What the X-Frame-Options header does

When a browser loads a page that another document has embedded through <iframe>, <frame>, <embed> or <object>, it checks the response headers of the embedded page before rendering it. If the response carries X-Frame-Options: DENY, the browser refuses to draw the page inside the embedding document. With SAMEORIGIN, it renders the page only if the embedding chain comes from the same origin (same scheme, host and port). Modern browsers evaluate the entire chain of ancestor frames, not just the top window.

The header was formalized in RFC 7034 in October 2013, after browsers had already shipped it. It remains one of the most widely deployed security headers: the HTTP Archive Web Almanac 2025 security chapter measures X-Frame-Options on roughly 35% of mobile sites, making it a top three security header behind X-Content-Type-Options (close to 50%).

Clickjacking in 60 seconds

Clickjacking was named by security researchers Robert Hansen and Jeremiah Grossman in 2008. The attack is simple: an attacker's page loads your site in an iframe, makes the iframe fully transparent with CSS, and positions it over harmless looking buttons. The visitor believes they are clicking "Play video", but the click actually lands on your site's "Delete account", "Confirm payment" or "Authorize app" button, using the victim's logged-in session. OWASP documents variants ranging from Facebook "likejacking" to hijacked one-click purchases.

Framing control is the defense. If the browser refuses to render your page inside an attacker's frame, the overlay trick collapses.

Developer reviewing web server configuration code on a laptop screen

The two valid values, and the ones to avoid

Value Effect Status in 2026
DENY No site may frame the page, including your own Valid, supported by all browsers
SAMEORIGIN Only pages from the same origin may frame it Valid, supported by all browsers
ALLOW-FROM uri Was meant to allow one named origin Obsolete: Firefox removed it in version 70 (October 2019), Chrome and Safari never supported it
ALLOWALL Never part of any spec Invalid: browsers ignore the header entirely, leaving no protection

The dangerous part is how browsers handle invalid values: they ignore the whole header. A site sending ALLOW-FROM or ALLOWALL believes it is protected while browsers treat it as if no header were set. As the Web Almanac 2025 puts it, these values "may have been set by developers expecting protections to be active due to them setting the header."

What real-world data shows

Across millions of sites measured by HTTP Archive in 2025, the header's values break down like this:

Bar chart showing the distribution of X-Frame-Options values on mobile sites in 2025: SAMEORIGIN 72.1 percent, DENY 24.6 percent, ALLOWALL 0.7 percent, other or invalid values 2.5 percent

Source: HTTP Archive Web Almanac 2025, security chapter, figure 9.30 (mobile dataset).

About 72.1% of sites that send the header choose SAMEORIGIN and 24.6% choose DENY. Roughly 3% send values that do nothing, which on the scale of the web is hundreds of thousands of sites with imaginary protection. Checking what your server actually sends takes less than a minute.

X-Frame-Options vs CSP frame-ancestors

The W3C Content Security Policy Level 2 specification (a W3C Recommendation since December 2016) introduced the frame-ancestors directive and formally obsoleted X-Frame-Options. Where XFO is a blunt on/off switch, frame-ancestors is a full allowlist:

Capability X-Frame-Options CSP frame-ancestors
Block all framing DENY frame-ancestors 'none'
Allow own origin only SAMEORIGIN frame-ancestors 'self'
Allow named partner origins Not possible (ALLOW-FROM is dead) frame-ancestors 'self' https://partner.example.com
Wildcard subdomains Not possible frame-ancestors https://*.example.com
Spec status Informational RFC 7034 (2013), obsoleted W3C CSP Level 2 (2016), current standard
When both headers are present Ignored by CSP2-capable browsers Takes precedence

Two details worth knowing. First, MDN notes that frame-ancestors does not inherit from default-src: a policy of default-src 'none' still allows anyone to frame the page, so you must declare the directive explicitly. Second, all current versions of Chrome, Edge, Firefox and Safari support frame-ancestors, so the only reason to keep sending X-Frame-Options is defense in depth for very old clients. That costs one line, so keep it.

How to configure it on your server

Send the header on every HTML response, exactly once. The examples below set SAMEORIGIN plus the equivalent CSP directive.

nginx (inside server or location):

add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
Enter fullscreen mode Exit fullscreen mode

Apache 2.4 (httpd.conf or .htaccess, mod_headers enabled):

Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self';"
Enter fullscreen mode Exit fullscreen mode

IIS (web.config):

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="X-Frame-Options" value="SAMEORIGIN" />
      <add name="Content-Security-Policy" value="frame-ancestors 'self';" />
    </customHeaders>
  </httpProtocol>
</system.webServer>
Enter fullscreen mode Exit fullscreen mode

Node.js with Express and Helmet:

const helmet = require("helmet");
app.use(helmet.frameguard({ action: "sameorigin" }));
app.use(helmet.contentSecurityPolicy({
  directives: { frameAncestors: ["'self'"] }
}));
Enter fullscreen mode Exit fullscreen mode

Behind a CDN such as Cloudflare you can also inject both headers with a response header transform rule, which is useful when you cannot touch the origin server. If a partner site legitimately needs to embed you, keep XFO at SAMEORIGIN and express the allowlist in CSP: frame-ancestors 'self' https://partner.example.com.

Mistakes that silently disable the protection

  • Using ALLOW-FROM in 2026. Every current browser ignores it, and with it the whole header.
  • Setting the header in a meta tag. X-Frame-Options only works as an HTTP header, and frame-ancestors is explicitly forbidden inside <meta http-equiv="Content-Security-Policy">. Both must come from the server.
  • Sending the header twice. Duplicate or comma-joined values such as SAMEORIGIN, SAMEORIGIN (0.28% of sites in the Almanac 2025 data) can be rejected as invalid. Set it in one place only, either the app or the web server, not both.
  • Using DENY on pages you embed yourself. If your own checkout, widget or preview runs in an iframe, DENY breaks it. Use SAMEORIGIN or an explicit frame-ancestors allowlist.
  • Assuming default-src covers framing. It does not. Declare frame-ancestors explicitly.

How to test your configuration

Open your browser's developer tools, load your page, and inspect the response headers on the main document: you should see exactly one X-Frame-Options and a Content-Security-Policy containing frame-ancestors. For the full picture, the free FortifyNet scan checks your framing protection together with the rest of your security header set, SSL/TLS, DNS and email authentication in about 60 seconds, and tells you exactly which header to add where. No signup needed.

Related guides

FAQ

Is X-Frame-Options deprecated?
Formally yes: W3C CSP Level 2 obsoleted it in favor of frame-ancestors back in 2016. In practice every browser still honors it, and OWASP recommends sending both headers for defense in depth. The one thing you must not do is rely on ALLOW-FROM.

Should I use DENY or SAMEORIGIN?
Use DENY if nothing on your site is ever shown in a frame, which is the strongest setting. Use SAMEORIGIN if your own pages embed each other, for example dashboards, previews or internal widgets. In the 2025 Web Almanac data, 72.1% of sites choose SAMEORIGIN.

How do I allow one specific partner site to embed my pages?
X-Frame-Options cannot do this anymore. Use Content-Security-Policy: frame-ancestors 'self' https://partner.example.com and list each allowed origin explicitly.

What happens if I send both X-Frame-Options and frame-ancestors?
Browsers that support CSP Level 2, which is all modern ones, enforce frame-ancestors and ignore X-Frame-Options. Older clients fall back to X-Frame-Options. That fallback chain is exactly why sending both is the recommended setup.

Can I set X-Frame-Options with a meta tag?
No. Browsers only honor it as a real HTTP response header. If you cannot change server configuration, set it through your hosting panel, CDN rules or application middleware.

Originally published at fortifynet.com/blog/x-frame-options. I'm the founder of FortifyNet, a website security scanner; this article comes from our blog, so factor in that founder bias when you read any tool recommendations here.

Top comments (0)