Clickjacking Protection: X-Frame-Options vs CSP frame-ancestors

How clickjacking works, how X-Frame-Options and CSP frame-ancestors differ, which takes precedence, and configs for Nginx, Apache and Cloudflare.

By the MakeItFlow team Updated 6 min read
Share LinkedIn X Facebook WhatsApp
Clickjacking Protection: X-Frame-Options vs CSP frame-ancestors

Clickjacking turns your own pages against your users. An attacker loads your site in an invisible frame on top of a page they control, lines up a "Win a prize" button with your "Delete account" or "Confirm transfer" button, and the user clicks something they never saw. The fix is a single response header that tells browsers who may frame your pages. There are two headers that can do it, though, and they behave differently. This guide explains both, which one wins, and how to deploy them without breaking legitimate embeds.

Key takeaways

  • Stop clickjacking by telling browsers who may frame your pages with CSP frame-ancestors and X-Frame-Options headers.
  • When CSP frame-ancestors is enforced, browsers ignore X-Frame-Options, so send both and keep X-Frame-Options as a fallback.
  • For most business sites, frame-ancestors 'self' with X-Frame-Options SAMEORIGIN is the safe default.
  • Frame-ancestors only works as an HTTP response header and is ignored in a meta tag Content Security Policy.

How a clickjacking attack works

The attacker's page looks roughly like this:

<!-- attacker.example -->
<style>
  iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
           opacity: 0; z-index: 2; }
  .bait  { position: absolute; top: 310px; left: 420px; z-index: 1; }
</style>
<button class="bait">Claim your free gift</button>
<iframe src="https://your-site.example/account/settings"></iframe>

The visitor sees the bait button. Their click lands on the invisible frame, on whatever control of yours sits in that spot. If they're logged in to your site, the click is authenticated. Common targets are account settings, "follow" or "like" buttons, permission prompts, payment confirmations and admin actions.

The only reliable defense is to stop your pages from being framed by sites you don't trust. JavaScript "frame-busting" snippets were the old approach. OWASP documents several ways around them, including sandboxed iframes that simply disable your script, so treat them as a fallback at most.

Option 1: X-Frame-Options

The older header, which every browser in use today understands:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
  • DENY: the page can't be framed by anyone, including your own site.
  • SAMEORIGIN: only pages from the same origin may frame it.

Its limitations:

  • No partner allowlist. The old ALLOW-FROM uri value is obsolete. According to MDN, modern browsers that see it ignore the header entirely, leaving you with no protection at all.
  • One value only. You can't say "my own site and one partner".
  • Header only. <meta http-equiv="X-Frame-Options"> does nothing.

Option 2: CSP frame-ancestors

The modern replacement is a Content Security Policy directive:

Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self'
Content-Security-Policy: frame-ancestors 'self' https://partner.example https://*.trusted-cdn.example
  • 'none' is equivalent to DENY. The quotes are required.
  • 'self' is roughly equivalent to SAMEORIGIN.
  • You can list multiple origins, wildcard subdomains and schemes.

Things to know:

  • It checks every ancestor, not just the immediate parent. If your page is nested two frames deep, every frame above it must be allowed.
  • It doesn't fall back to default-src. A policy of default-src 'self' with no frame-ancestors still lets anyone frame your page. You must set it explicitly.
  • It's header only. Like X-Frame-Options, frame-ancestors is ignored in a <meta> CSP tag.
  • It's not the same as frame-src. frame-src controls which sites your page may embed. frame-ancestors controls who may embed your page.

MDN lists frame-ancestors as Baseline widely available since January 2018.

Which one wins if you send both?

The CSP specification says that when a frame-ancestors directive is enforced, browsers must ignore X-Frame-Options. OWASP's Clickjacking Defense Cheat Sheet repeats this and recommends sending both, because some old browsers only understood X-Frame-Options.

That gives you a neat pattern when you need to allow a partner:

Content-Security-Policy: frame-ancestors 'self' https://partner.example
X-Frame-Options: SAMEORIGIN

Modern browsers apply the CSP and allow the partner. Very old browsers fall back to SAMEORIGIN and block the partner, which fails safe rather than open.

Which value should you use?

Your situation frame-ancestors X-Frame-Options
Nothing on your site is ever framed 'none' DENY
You frame your own pages (previews, page builders, some admin UIs) 'self' SAMEORIGIN
A partner, app store or parent site embeds you 'self' https://partner.example SAMEORIGIN
A widget meant to be embedded anywhere (public video player, booking widget) Leave it off on that widget's URL only Leave it off on that URL only

For most business sites, 'self' / SAMEORIGIN is the safe default. 'none' can break WordPress page builders, live previews and some CMS editors that frame your own pages.

Configuration

Nginx

server {
    # ...
    add_header Content-Security-Policy "frame-ancestors 'self'" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Example: a public embed endpoint that anyone may frame
    location /embed/ {
        add_header Content-Security-Policy "frame-ancestors *" always;
        # No X-Frame-Options here. Defining add_header in this block
        # stops server-level add_header lines from being inherited.
    }
}

If you already send a full Content Security Policy, add frame-ancestors to that header instead of sending a second Content-Security-Policy header. Browsers enforce every CSP header they receive, so several policies can combine in surprising ways.

Apache

<IfModule mod_headers.c>
    Header always set Content-Security-Policy "frame-ancestors 'self'"
    Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>

<Location "/embed/">
    Header always set Content-Security-Policy "frame-ancestors *"
    Header always unset X-Frame-Options
</Location>

Cloudflare

Create a Response Header Transform Rule (under Rules) for your hostname with two Set static operations:

  • Content-Security-Policy = frame-ancestors 'self'
  • X-Frame-Options = SAMEORIGIN

Add a second rule, matched on the /embed/ path, that sets the embed-friendly value and removes X-Frame-Options. "Set" overwrites what your origin sends, so check whether your application already sends a full CSP before you replace it at the edge.

WordPress

WordPress already sends X-Frame-Options: SAMEORIGIN on the login page and admin screens. Your public pages usually don't get it unless your host, theme or a security plugin adds it. Set it at the server level with the snippets above, or see our WordPress security checklist.

Test your protection

Check the headers:

curl -sI https://example.com/account | grep -iE "x-frame-options|content-security-policy"

Then do a real test. Save this as a local HTML file, open it in a browser, and replace the URL with a page on your own site:

<!doctype html>
<title>Frame test</title>
<p>If the box below is empty or shows an error, framing is blocked.</p>
<iframe src="https://example.com/account" width="800" height="500"></iframe>

If your page renders inside the frame, it's frameable. The browser console will name the header that blocked it when protection works. Test the pages that matter: login, account settings, checkout, admin.

What framing headers don't cover

OWASP's cheat sheet flags a newer variant, DoubleClickjacking, which uses a separate top-level window instead of a frame, so framing headers don't stop it. For sensitive actions such as payments, deleting data and changing email or password, add a confirmation step the server enforces, like re-entering a password or a 2FA code. SameSite=Lax or Strict cookies also help: they aren't sent in cross-site iframes, so a framed page loads logged out.

Frequently asked questions

Is X-Frame-Options deprecated?

It isn't formally removed, and browsers still honor it, but CSP frame-ancestors supersedes it. Send both: frame-ancestors for modern browsers, X-Frame-Options as a fallback for older ones.

Why does my scan say X-Frame-Options is missing when I have frame-ancestors?

Some scanners check for both. If your frame-ancestors is set correctly, modern browsers are protected. Adding a matching X-Frame-Options value costs nothing and covers older clients.

Can I set frame-ancestors in a meta tag?

No. Browsers ignore frame-ancestors in <meta http-equiv="Content-Security-Policy">. It must be an HTTP response header, set by your server, CDN or application.

Does this affect embedding YouTube videos or maps on my site?

No. Those headers control who can frame your pages. Embedding other sites in your pages is governed by their headers and, if you have a CSP, by your frame-src directive.

What to do next

Add frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN site-wide, carve out exceptions only for URLs designed to be embedded, and run the iframe test on your login and account pages. Run a free audit to confirm both headers are present across your site, and see the Content Security Policy guide if you want to extend frame-ancestors into a full policy.

Sources

#clickjacking #frame-ancestors #x-frame-options #content security policy #security headers

Written by the MakeItFlow team

We build the website audit these guides are based on. Every fact is checked against the primary sources listed above and this guide was last reviewed on 21 October 2026 . How we research and check

Share LinkedIn X Facebook WhatsApp

Check your own site in 30 seconds

Run a free audit covering speed, SEO, DNS, email authentication and security headers — and get a prioritised fix list.

Weekly newsletter

One practical website fix, every week

New guides on speed, SEO, email deliverability and security — straight to your inbox. No spam, unsubscribe in one click.

Keep reading