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-Optionsin 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.