All guides · Security

Clickjacking Protection with frame-ancestors

Clickjacking is an attack where another website loads your page inside an invisible or disguised frame, then lures the visitor into clicking on it. The visitor thinks they are pressing a button on the attacker’s page, but the click actually lands on your page, where they are logged in.

Why it matters

Any action that takes one or two clicks can be triggered this way: changing account settings, confirming a payment, granting an app access, deleting data, or following someone. The attack needs no bug in your code. It only needs your page to be willing to load inside someone else’s frame.

The fix is a single response header that tells browsers which sites may frame your pages.

What we check

Check Passes when
Clickjacking protection an enforced CSP sets frame-ancestors without a * source, or X-Frame-Options is DENY or SAMEORIGIN

A frame-ancestors directive in a Report-Only policy does not count, because it does not block anything. X-Frame-Options values other than DENY and SAMEORIGIN, such as the obsolete ALLOW-FROM, do not count either: current browsers ignore them.

How to fix

Use the CSP frame-ancestors directive. It is the current standard, it overrides X-Frame-Options in browsers that support both, and it can list specific trusted sites.

If your pages never need to be framed:

Content-Security-Policy: frame-ancestors 'none'

If only your own site frames them (for example in an admin preview):

Content-Security-Policy: frame-ancestors 'self'

If a partner site must embed them:

Content-Security-Policy: frame-ancestors 'self' https://partner.example

If you already send a Content Security Policy, add frame-ancestors to that same header rather than sending a second one. When a response carries two CSP headers, browsers apply both, and a resource has to pass every policy.

frame-ancestors only works in an HTTP header. Browsers ignore it inside a <meta http-equiv="Content-Security-Policy"> tag.

For very old browsers that do not support frame-ancestors, you can also send X-Frame-Options. It costs nothing and does not conflict as long as both say the same thing:

X-Frame-Options: DENY

Use SAMEORIGIN instead of DENY if you chose frame-ancestors 'self'.

Nginx

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

Note that Nginx does not merge add_header directives: if a location block sets any add_header, it drops all the ones inherited from the server block. Repeat your security headers there, or keep them in a shared file you include.

Apache

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

Cloudflare: add the headers with a Response Header Transform Rule (Rules → Transform Rules → Modify Response Header).

Pages that are meant to be embedded

Widgets, video players and embeddable forms need to be framed by other sites. Serve them from their own paths or a separate subdomain and set a permissive frame-ancestors only there. Keep the rest of the site, especially login, account and checkout pages, on 'self' or 'none'. Make sure embeddable pages do not perform sensitive actions on a single click.

Common mistakes

  • Setting X-Frame-Options in a <meta> tag. Browsers ignore it there; it must be a header.
  • Sending the header only on the home page. Protect every page, especially those with forms and account actions.
  • Using frame-ancestors *. It allows every site to frame the page, the same as having no protection.

Further reading: OWASP: Clickjacking Defense Cheat Sheet and MDN: frame-ancestors.

Last updated October 24, 2026