# X-Content-Type-Options, X-Frame-Options and frame-ancestors

> Two headers that cost one line each. nosniff stops browsers running uploads as scripts; X-Frame-Options and frame-ancestors stop clickjacking. Configs for nginx, Apache, Caddy, WordPress and Next.js.

Updated 2026-09-25 · Security · HTML version: https://getreport.app/guides/x-content-type-options-x-frame-options-frame-ancestors

Two of the six [security headers](https://getreport.app/learn/security-headers) every scanner grades are the cheapest to add and the hardest to get wrong: `X-Content-Type-Options: nosniff`, which has one value and no downside, and framing protection, which is either `X-Frame-Options` or the `frame-ancestors` directive of a Content-Security-Policy. This guide explains what each one prevents with a concrete attack, when your own site needs to be framed (page builders, previews, embeds) and how to allow that without opening the door, and gives the config for every common server and platform. Twenty minutes, one reload, two findings gone.

## Quick answer

```text
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
```

- `nosniff` on every response, always. There is no site that should not send it.
- `X-Frame-Options: DENY` if nothing ever frames your pages, `SAMEORIGIN` if your own site does (WordPress's Customizer and most page builders do). `ALLOW-FROM` is dead; browsers ignore it.
- `frame-ancestors` in CSP is the modern version: it takes a list of origins and overrides `X-Frame-Options` where both are present. Send both for older browsers.
- Set them at the web server or CDN, once. Not in `<meta>` tags: neither header works there.
- Run the [security headers checker](https://getreport.app/tools/security-headers) before and after.

## Why these two headers matter

### nosniff: stop the browser guessing

Browsers were built to be forgiving. A server that says a file is `text/plain` might be wrong, so the browser looks at the bytes and, if they look like JavaScript, may run them as JavaScript. That forgiveness is the attack. A comment form that accepts file uploads stores `avatar.txt`; the file contains a script; an attacker links to it in a `<script src>` on their page or gets a victim to open it directly; the browser sniffs, decides it is script or HTML, and runs it on your origin, with your visitors' cookies. The same trick turns an "image" upload into a page that renders as HTML.

`X-Content-Type-Options: nosniff` tells the browser to believe the `Content-Type` header. A `<script>` whose response is not a JavaScript MIME type is not executed; a `<link rel="stylesheet">` whose response is not `text/css` is not applied; a `text/plain` file is shown as text. The header also lets the browser apply Cross-Origin Read Blocking more aggressively, so another origin cannot pull your HTML or JSON into its page through a resource tag. It has been supported in every major browser for years, and nothing legitimate breaks, provided your server sends correct MIME types, which it should anyway.

### Framing: stop clickjacking

[Clickjacking](https://getreport.app/learn/clickjacking) loads your page inside an invisible `<iframe>` on the attacker's page, positions your "Confirm order", "Delete account" or "Follow" button under a fake button the visitor wants to press, and lets the visitor's own click, with their own logged-in session, do the damage. No credentials are stolen; the visitor did it themselves.

The defence is to tell browsers who may frame the page. `X-Frame-Options`, the older header, has two working values: `DENY` (nobody) and `SAMEORIGIN` (only pages from the same scheme, host and port). CSP's `frame-ancestors` directive, added in CSP Level 2, does the same with a list: `'none'`, `'self'`, or specific origins such as `https://partner.example`. When a response carries both, browsers that understand `frame-ancestors` use it and ignore `X-Frame-Options`, so the two can be sent together without conflict.

## How getReport checks it

> **Free tool:** [Security headers checker](https://getreport.app/tools/security-headers): Check HSTS, CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy and cookie flags on any site. Free, no signup, with a fix for every missing header.

The checker fetches the page like a browser, following redirects, and grades the final response's headers. For `nosniff` the test is exact: the header must be present and its value must be `nosniff` (any case); anything else, including a typo, is a finding. For framing, the check passes if `X-Frame-Options` is `DENY` or `SAMEORIGIN`, or if an **enforcing** `Content-Security-Policy` has a `frame-ancestors` directive whose source list is not `*`. A `frame-ancestors` in a report-only policy does not count, because report-only policies do not block anything.

> **Check: X-Content-Type-Options: nosniff is set.** Without it, browsers may guess a file's type from its content and run an uploaded image or text file as a script.
>
> 1. Send the header: X-Content-Type-Options: nosniff on every response.

> **Check: Other sites cannot embed this page in a frame.** A site that loads your page in an invisible frame can trick visitors into clicking your buttons (clickjacking), for example "confirm order" or "delete account".
>
> 1. Add frame-ancestors 'self' to your Content-Security-Policy, or send X-Frame-Options: SAMEORIGIN.
> 2. Use frame-ancestors 'none' (or X-Frame-Options DENY) if the page never needs to be embedded.

![The X-Content-Type-Options finding opened on a page with no security headers: the header shown as absent, the explanation of MIME sniffing, and the one-line fix for the server](https://getreport.app/guides/img/x-content-type-options-x-frame-options-frame-ancestors/frame-options.webp "Each missing header is one finding with the header's observed value and the fix.")

The evidence line under each finding shows what was received: `x-content-type-options: (absent)`, or `x-frame-options: (absent); frame-ancestors: (absent)`. If it shows a value you did not set (Cloudflare, a host or a plugin added it), that is your clue that two places are setting headers.

`frame-ancestors` lives inside the CSP, so a site that has no CSP at all also gets the separate CSP finding. You can fix framing with `X-Frame-Options` alone today and grow the CSP later:

> **Check: Content-Security-Policy header is set.** A CSP blocks most cross-site scripting attacks by listing where scripts may load from. Without one, a single injected script can steal sessions or card data.
>
> 1. Start in report-only mode with Content-Security-Policy-Report-Only to see what would break.
> 2. Move to an enforcing policy once the report is quiet; keep "unsafe-inline" out of script-src.

## Step by step

### 1. Decide DENY or SAMEORIGIN

Ask whether any page of your own site loads another page of your site in an iframe:

| Situation | Value |
| --- | --- |
| Plain site, no builder, no previews | `DENY` / `frame-ancestors 'none'` |
| WordPress (Customizer preview, block editor previews, Elementor and most builders) | `SAMEORIGIN` / `frame-ancestors 'self'` |
| Your page is embedded on a partner site as a widget | `frame-ancestors 'self' https://partner.example` and no `X-Frame-Options` for that path |
| A public embed anyone may frame (a map, a badge) | No framing header on that path only, or `frame-ancestors *` on it, and keep the rest of the site protected |

`SAMEORIGIN` is the right default for anything with an admin that previews pages. `DENY` on a WordPress site breaks the Customizer and page-builder previews, which show a blank frame and a console error. WordPress itself sends `X-Frame-Options: SAMEORIGIN` on `wp-admin` and the login page; the front end is what you are adding.

### 2. Add both headers at the server

**nginx**, in the `server` block; `always` so error pages carry them too:

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

If a `location` block has its own `add_header`, it replaces the server-level ones for that location; repeat them there.

**Apache**, in `.htaccess` at the site root or the virtual host, with `mod_headers` enabled:

```apache
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"
```

**Caddy**:

```caddy
example.com {
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Content-Security-Policy "frame-ancestors 'self'"
    }
    reverse_proxy app:3000
}
```

**Cloudflare**: Rules → Transform Rules → Modify Response Header → Set static header, one rule per header, "All incoming requests". Cloudflare's Managed Transforms also has an "Add security headers" switch that adds a fixed set including `nosniff`; use the explicit rule when you need `SAMEORIGIN` rather than the managed value. If the origin already sends the same header, remove it from one side so browsers see one value.

**Next.js**, in `next.config.js`:

```js
// next.config.js — applied to every route, including static files
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
          { key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
        ],
      },
    ];
  },
};
```

### 3. WordPress without server access

A small plugin or the child theme's `functions.php`:

```php
<?php
// Sends the two framing headers and nosniff on every front-end response PHP serves.
add_action('send_headers', function () {
    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: SAMEORIGIN');
    header("Content-Security-Policy: frame-ancestors 'self'");
});
```

Headers set in PHP only reach responses PHP produces. Static files and pages served from a full-page cache (a caching plugin, the host's cache, Cloudflare) may not carry them, so the server or CDN configuration is better whenever you can get at it. Several security plugins expose the same three headers as switches; use one source, not two.

### 4. Add the CSP directive without a full CSP

`frame-ancestors 'self'` is a complete, valid Content-Security-Policy on its own. It does nothing about scripts or styles, so it cannot break anything, and it satisfies the framing check. When you later roll out a full policy, keep the directive in it; the full procedure is in [Content-Security-Policy from report-only to enforced](https://getreport.app/guides/content-security-policy-rollout). Note that `frame-ancestors` is one of the directives that cannot be set through a `<meta http-equiv>` tag; it must be a header.

### 5. Allow a partner to frame one path

A widget at `/embed/` that partners include on their sites needs a looser rule on that path only:

```nginx
location /embed/ {
    add_header X-Content-Type-Options "nosniff" always;
    # No X-Frame-Options here: it cannot express "these two origins"
    add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example https://other-partner.example" always;
}
```

Because `frame-ancestors` overrides `X-Frame-Options` in every current browser, sending `X-Frame-Options: SAMEORIGIN` alongside it would only affect browsers too old to know CSP 2, which are rare enough to ignore for a widget. `ALLOW-FROM` was the old way to name one origin; Firefox dropped it and Chrome never supported it, so it is now equivalent to sending nothing.

### 6. Reload and re-run

Both headers apply on the next response; there is no cache to wait for unless a CDN caches full pages, in which case purge it. Re-run the checker: both findings should pass and the evidence line should show the values you set. The remaining four headers in the same panel are covered in [Security headers from zero to A](https://getreport.app/guides/security-headers-from-zero).

## Platform notes

**Shopify, Wix, Squarespace**: the platform sets framing protection and `nosniff` for you and does not let you change them; the checker shows them as passing. **Netlify**: a `_headers` file in the publish directory with the same three lines under `/*`. **Vercel**: `headers` in `vercel.json`, or the Next.js config above. **Cloudflare Pages**: `_headers` as on Netlify.

## Verify

- The security headers checker shows "X-Content-Type-Options: nosniff is set" and "Other sites cannot embed this page in a frame".
- `curl -sI https://example.com/ | grep -i -E "x-content-type|x-frame|content-security"` prints exactly one line per header, with the values you chose.
- In the browser, DevTools → Network → the document request → Headers lists them under Response Headers. To test framing, open a page on another origin that iframes yours; the console shows "Refused to display … in a frame" and the frame stays blank.
- Nothing you rely on broke: the WordPress Customizer preview loads, the page-builder editor works, partner embeds still render on the partner's site.

## Common mistakes

- **`DENY` on a site whose admin previews pages.** Symptom: a blank Customizer or builder preview and a "Refused to display" console error. Fix: `SAMEORIGIN` / `'self'`.
- **`ALLOW-FROM` for a partner.** Ignored by every current browser, so the page is unprotected. Use `frame-ancestors` with the origin list.
- **`frame-ancestors` only in a report-only policy.** It reports and blocks nothing; the checker does not count it. Put it in the enforcing header.
- **Headers in `<meta http-equiv>`.** `X-Content-Type-Options` and `X-Frame-Options` do not work as meta tags, and `frame-ancestors` is explicitly excluded from meta CSP. Send real headers.
- **Two sources with different values.** A plugin sends `SAMEORIGIN`, Cloudflare sends `DENY`. Browsers see both headers; Chrome treats a conflicting pair as `DENY`, which breaks the admin preview, and other browsers differ. Keep one source.
- **`nosniff` on the HTML page but wrong MIME types on assets.** Once sniffing is off, a stylesheet served as `text/plain` is dropped. Fix the server's MIME map (`.css` → `text/css`, `.js` → `text/javascript`, `.woff2` → `font/woff2`) rather than removing the header.
