Skip to content

SecurityPart of: Security headers

X-Frame-Options, frame-ancestors and nosniff: stop clickjacking

X-Frame-Options and CSP frame-ancestors stop other sites framing your pages for clickjacking; X-Content-Type-Options nosniff stops MIME sniffing. What to send and where to set it.

getReport teamUpdated 27 Sept 202610 min read

X-Frame-Options is a response header that tells browsers whether other sites may show your page inside a frame: DENY means nobody, SAMEORIGIN means only your own site. It stops clickjacking, and its modern replacement is the frame-ancestors directive of a Content-Security-Policy, which you can send alongside it. Its neighbour X-Content-Type-Options: nosniff stops browsers guessing file types and running an upload as a script. Both are one line each and part of the six headers in HTTP security headers from zero to A. This guide explains the attacks, when your own site needs framing (page builders, previews, embeds), and the config for every common server.

Quick answer

Text
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
  • nosniff on every response, always. No site should go without 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 takes a list of origins and overrides X-Frame-Options where both are present. Send both.
  • Set them at the web server or CDN. Neither works in a <meta> tag.
  • Run the security headers checker before and after.

What clickjacking is and how X-Frame-Options stops it

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 has two working values: DENY (nobody) and SAMEORIGIN (only pages from the same scheme, host and port). CSP's frame-ancestors directive 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. The CSP specification treats frame-ancestors as the successor, but X-Frame-Options is still honoured by every browser and still expected by scanners.

What X-Content-Type-Options: nosniff does

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 or HTML, may treat them that way. That forgiveness is the attack. A form that accepts uploads stores avatar.txt containing a script; an attacker references it from a <script src> or gets a victim to open it; the browser sniffs, decides it is script or HTML, and runs it on your origin, with your visitors' cookies.

nosniff tells the browser to believe the Content-Type header. A <script> whose response is not a JavaScript MIME type is not executed; a stylesheet not served as text/css is not applied. It 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. Nothing legitimate breaks, provided your server sends correct MIME types.

How getReport checks it

The checker fetches the page like a browser, following redirects, and grades the final response. 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 block nothing. For nosniff the test is exact: the header must be present with the value nosniff (any case).

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
Each missing header is one finding with the header's observed value and the fix.

The evidence line shows what was received, such as x-frame-options: (absent); frame-ancestors: (absent). A value you did not set means another layer (Cloudflare, the host, a plugin) is adding headers too. A site with no CSP at all also gets the separate CSP finding; fix framing with X-Frame-Options today and grow the CSP later:

Step by step

1. Decide DENY or SAMEORIGIN

SituationValue
Plain site, no builder, no previewsDENY / frame-ancestors 'none'
WordPress (Customizer, block editor previews, Elementor and most builders)SAMEORIGIN / frame-ancestors 'self'
Your page is a widget embedded on a partner siteframe-ancestors 'self' https://partner.example, no X-Frame-Options on that path
A public embed anyone may frame (a map, a badge)No framing header on that path only, 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 the 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:

Caddyfile
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. 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. If the origin already sends the same header, remove it from one side.

Next.js, in next.config.js:

JavaScript
// 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 may not carry them, so the server or CDN configuration is better whenever you can reach it. Several security plugins expose the same headers as switches; use one source, not two.

4. Add frame-ancestors without a full CSP

frame-ancestors 'self' is a complete, valid Content-Security-Policy on its own. It says 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 procedure is in Content Security Policy: roll it out without breaking your site. frame-ancestors 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;
}

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; purge the CDN if it caches full pages. Re-run the checker: both findings should pass and the evidence line should show your values.

Platform notes

Shopify, Wix, Squarespace: the platform sets framing protection and nosniff and does not let you change them; the checker shows what they send. Netlify and Cloudflare Pages: a _headers file in the publish directory with the same three lines under /*. Vercel: headers in vercel.json, or the Next.js config above. Both hosts are covered in Netlify and Vercel headers and caching.

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.
  • 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.
  • The WordPress Customizer preview loads, the page-builder editor works, and partner embeds still render on the partner's site.

Common mistakes

  • DENY on a site whose admin previews pages. Blank previews and a "Refused to display" error. Use 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. Put it in the enforcing header.
  • Headers in <meta http-equiv>. Neither header works as a meta tag, and frame-ancestors is excluded from meta CSP.
  • Two sources with different values. A plugin sends SAMEORIGIN, Cloudflare sends DENY. Under the HTML standard, conflicting X-Frame-Options values block all framing, like DENY, which breaks the admin preview. Keep one source.
  • nosniff with wrong MIME types on assets. Once sniffing is off, a stylesheet served as text/plain is dropped. Fix the MIME map (.css → text/css, .js → text/javascript, .woff2 → font/woff2) rather than removing the header.

Questions people ask

What is clickjacking?

Clickjacking is an attack where another site loads your page in an invisible frame and tricks visitors into clicking your buttons while they think they are clicking something on the attacker's page. Because the click happens in the visitor's own logged-in session, it can confirm an order, change a setting or grant a permission. X-Frame-Options or CSP frame-ancestors stops it by refusing the frame.

Should I use X-Frame-Options DENY or SAMEORIGIN?

Use SAMEORIGIN if any page of your own site shows another in a frame, which includes the WordPress Customizer, block editor previews and most page builders. Use DENY only when nothing ever frames your pages, such as a plain static site. DENY on a site with previews breaks them with a blank frame. Both stop other sites from framing you.

Is X-Frame-Options deprecated?

Not in practice. The CSP specification makes frame-ancestors the modern replacement, and browsers that support both prefer it, but every current browser still honours X-Frame-Options and security scanners still look for it. Send both: X-Frame-Options: SAMEORIGIN and Content-Security-Policy: frame-ancestors 'self'. Only the ALLOW-FROM value is obsolete.

Can X-Content-Type-Options: nosniff break my site?

Only if your server sends wrong Content-Type values. With nosniff, a script served as text/plain or a stylesheet served as application/octet-stream is refused instead of guessed. Check DevTools → Network for blocked files after adding it, and fix the server's MIME types rather than removing the header. Sites with a correct MIME map see no change at all.

Check your site before and after Check