Security guide
An attacker does not need a password if they can get your visitor to click the right button without knowing it. This guide covers how the attack works, the two headers that prevent it, and which of them you still need today.
01 Why it matters
Clickjacking loads your site into an invisible frame on a page the attacker controls. Your page loads normally, with the visitor already logged in, is then made transparent and positioned underneath something they do want to click: a play button, a prize, a cookie notice. The click is real, the session is real, and it lands on your button. Delete account, confirm transfer, grant access: whatever a single click can do on your site can now be done by someone who never saw it.
Afterwards nothing on your side looks wrong. The request carried a valid session, came from a real browser and did what it was told, so neither your logs nor your fraud rules have anything to flag. That is why this is solved by refusing to be framed at all, rather than by detecting the attack.
02 The headers
X-Frame-Options: DENY What it does. Tells the browser to refuse to render your page in any frame or iframe, including one on your own site.
Why it matters. The strongest and simplest value. If nothing on your site needs to embed your own pages, this is the setting to choose, and it spares you any judgement about which origins to trust.
add_header X-Frame-Options "DENY" always; X-Frame-Options: SAMEORIGIN What it does. Allows framing, but only by pages on the same origin as the page being framed.
Why it matters. The right value when your own application embeds its own pages, in a preview or an editor for example. Everything from outside is still refused.
add_header X-Frame-Options "SAMEORIGIN" always; X-Frame-Options: ALLOW-FROM What it does. Was meant to name a single origin allowed to frame the page. No current browser implements it.
Why it matters. Setting it narrows nothing: a browser that does not understand the value ignores the header completely, so a page that looks protected is not. If you need to allow one specific partner, that is what frame-ancestors is for.
add_header X-Frame-Options "ALLOW-FROM https://partner.example" always; frame-ancestors 'none' What it does. The Content-Security-Policy directive that supersedes X-Frame-Options. It lists the origins allowed to embed the page, and none allows nobody.
Why it matters. Where both headers are present, this is the one browsers follow, and it is the only one that can name more than a single origin. Send it as part of your policy even if you already send X-Frame-Options.
add_header Content-Security-Policy "frame-ancestors 'none'" always; frame-ancestors 'self' https://partner.example What it does. Allows your own origin plus the origins you name, and refuses everyone else.
Why it matters. The case ALLOW-FROM was supposed to cover, done in a way that actually works. Name each origin in full, including the scheme.
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example" always; 03 Rolling it out
Settle the answer first: does anything legitimately embed your pages, and if so, from which origins? Then send frame-ancestors for current browsers and X-Frame-Options with the matching value for older clients. Keeping the two in agreement matters more than which one you consider primary, because a mismatch is how a partner integration breaks in one browser and not in the other.
server { add_header X-Frame-Options "DENY" always; add_header Content-Security-Policy "frame-ancestors 'none'" always; } The header only protects the page that sends it, so it belongs on every response and not just on the login page. If you already send a Content-Security-Policy, add frame-ancestors to that existing policy rather than sending a second one. And check the result on the live site: a reverse proxy or CDN that adds its own X-Frame-Options leaves the header there twice, which some browsers treat as invalid and ignore.
04 Questions
Superseded, not useless. Where both are present, browsers follow frame-ancestors. Sending X-Frame-Options as well costs one line and covers clients that do not evaluate the directive, which is why our scan expects both.
No. Same origin means the same scheme, host and port, so a page on one subdomain cannot frame a page on another under SAMEORIGIN. That case needs frame-ancestors with both origins named.
frame-ancestors with your own origin and the partner origin, plus X-Frame-Options: SAMEORIGIN alongside it. Old clients will refuse the partner frame, and there is no working way to allow a foreign origin through X-Frame-Options: that is precisely what ALLOW-FROM failed to deliver.
No. The framed page is your own, so it carries a valid token like any other page the visitor has open. The token proves the request came from your page, and in this attack it genuinely did. Only refusing the frame helps.
05 In depth
Framing is one way a page can be turned against its visitors. These guides cover the others.
What each response header does, which ones your site should send and how to configure them correctly.
The strongest defence against cross-site scripting: which directives to set, how to roll a policy out in report-only mode and how to reach an enforcing policy without breaking your site.
Strict-Transport-Security explained: what max-age, includeSubDomains and preload actually do, and why the preload list is a decision you cannot quickly undo.
Secure, HttpOnly and SameSite: the cookie attributes that keep a session out of reach of scripts and cross-site requests, with the settings for a typical stack.
Which part of your URLs travels to other sites when a visitor clicks away: the five policy values that matter, what each one gives up and the one to set by default.
Switch off camera, microphone, geolocation and payment for your site and everything it embeds: the allow-list syntax, and why a feature you did not name is not a feature you turned off.
The records that stop someone sending mail in your name, and the one that keeps your DNS answers honest: what each does and the order to introduce them in.
The file that tells a researcher where to report a vulnerability. Two mandatory fields, ten minutes of work, and the difference between a private report and a public one.
A header that is right today can be gone after the next deploy. Three ways to notice: a cron job with curl, a check in your CI pipeline and a scheduled scan, with what each of them catches and misses.
How a policy changes quietly through deploys, plugins and CDN rules, and how to catch it: a header diff, violation reports through report-to, and what a scan comparison can and cannot show.
Keep reading
Every guide stands on its own, and together they cover what our scanner looks at. Pick the one closest to your next question.
Our free Quickscan checks X-Frame-Options and frame-ancestors on your live response alongside the rest of your security headers, and explains every finding in plain language.