# X-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.

Source: https://howhttpworks.com/headers/x-frame-options
Last reviewed: 2026-10-05

> **TL;DR:** `X-Frame-Options: DENY` blocks framing; `SAMEORIGIN` permits it only when every ancestor has the framed document's origin. To allow named partner origins, use CSP `frame-ancestors`. `ALLOW-FROM` is 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](https://html.spec.whatwg.org/multipage/speculative-loading.html#the-x-frame-options-header).

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:

```http
X-Frame-Options: DENY
```

Or:

```http
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://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options#directives).

`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:

```http
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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) is a separate boundary.

A controlled demonstration can use an iframe over a visible decoy button:

```html
<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:

```http
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](https://w3c.github.io/webappsec-csp/#directive-frame-ancestors) 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:

```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
```

For Apache with `mod_headers` loaded:

```apacheconf
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](https://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header) and [Apache header tables](https://httpd.apache.org/docs/2.4/mod/mod_headers.html#header) if the application or proxy also sets these fields.

With Helmet installed in an existing Express app:

```javascript
const helmet = require('helmet')
app.use(helmet.xFrameOptions({ action: 'deny' }))
```

[Helmet's option](https://helmet.js.org/#x-frame-options) 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:

```caddyfile
app.example.com {
    header {
        X-Frame-Options DENY
        Content-Security-Policy "frame-ancestors 'none'"
    }
    reverse_proxy localhost:3000
}
```

The [header directive](https://caddyserver.com/docs/caddyfile/directives/header) sets the response fields. If the application already owns CSP, add `frame-ancestors` there instead of replacing the policy at the proxy.

## Testing

```bash
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:

```html
<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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Frame-Options#browser_compatibility) 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

- [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy)
- [Cross-Origin-Opener-Policy](https://howhttpworks.com/headers/cross-origin-opener-policy)
