Clickjacking is one of the older browser attacks, but it never fully went away — and the defenses against it are small, cheap to add, and easy to get subtly wrong. The fix is to tell browsers, with response headers, whether other sites are allowed to load your pages inside a frame. This article walks through the two headers that do that job: X-Frame-Options (the legacy one) and the CSP frame-ancestors directive (the modern one), and how to use them together correctly.
For the site owner (plain English)
For non-technical owners: clickjacking is a trick where an attacker hides your real page inside an invisible frame on their own site, so a visitor's clicks secretly land on your site while they believe they are clicking something else — potentially triggering actions such as changing a setting or confirming a payment. The defence is a response header that tells browsers not to let other sites frame your pages. It is a small, safe setting your web host, CDN, or platform can add — no redesign needed — and it rarely has side effects unless you deliberately embed your own pages on another domain. To check whether your site is currently protected, run the security headers checker.
What is clickjacking?
Clickjacking — sometimes called a "UI redress" attack — works by loading your real website inside an invisible or disguised iframe on a page the attacker controls. The user thinks they are interacting with the attacker's harmless-looking page, but their clicks are actually landing on your site, which is layered transparently on top. Because the user is already logged in to your site in the same browser, those clicks carry their session and their authority.
A concrete scenario: imagine your application has a one-click "Delete account" or "Transfer funds" or "Authorize app" button. An attacker builds a page that says "Win a free prize — click here!" and places a large, attractive button in the middle. Behind that button, in a fully transparent iframe positioned pixel-for-pixel on top, sits your real settings page — with its destructive button aligned exactly under the decoy. The victim clicks the prize button, the click passes through to your framed page, and the sensitive action fires without the victim ever realizing they touched your site.
The defense is not to detect the trick at click time — that is fragile — but to refuse to be framed at all. If the browser will not render your page inside another site's iframe, the overlay attack simply cannot be constructed. That refusal is communicated with response headers, evaluated by the browser before it ever paints your content.
X-Frame-Options
X-Frame-Options (often abbreviated XFO) is the original anti-framing header. It is a single response header with one of two meaningful values:
DENY— no site may frame this page, not even the page's own origin. This is the strongest setting and the right default for pages that have no legitimate reason to be embedded.SAMEORIGIN— the page may be framed only by pages served from the same origin. Use this when your own application legitimately embeds its own pages in frames (for example, a dashboard widget) but you want to block everyone else.
Adding it looks like this:
X-Frame-Options: DENY
# or, if you frame your own pages:
X-Frame-Options: SAMEORIGINX-Frame-Options is considered legacy — it was never standardized as a formal specification in the way that CSP was, and its behavior across browsers had quirks. It is, however, still genuinely useful: it is universally supported, including by older browsers that may not fully honor the modern CSP directive. So while it is no longer the primary tool, it remains a worthwhile belt-and-suspenders layer.
CSP frame-ancestors
The modern, standardized replacement is the frame-ancestors directive of the Content-Security-Policy header. It controls which origins are permitted to embed your page as an ancestor — that is, in any <frame>, <iframe>, <object>, or <embed>. Unlike X-Frame-Options, it is part of the CSP standard, has well-defined behavior, and supports an allow-list of multiple origins. Its exact semantics are documented in the MDN frame-ancestors reference.
The three common forms:
frame-ancestors 'none'— equivalent toX-Frame-Options: DENY. No site may frame the page.frame-ancestors 'self'— equivalent toX-Frame-Options: SAMEORIGIN. Only the page's own origin may frame it.frame-ancestors https://app.example.com https://partner.example— an explicit allow-list. Only the named origins may frame the page; everyone else is blocked. This is somethingX-Frame-Optionscannot express.
In practice:
Content-Security-Policy: frame-ancestors 'none'
# allow only your own origin:
Content-Security-Policy: frame-ancestors 'self'
# allow your own origin plus a specific partner:
Content-Security-Policy: frame-ancestors 'self' https://partner.exampleWhere a browser supports it, frame-ancestors supersedes X-Frame-Options — the CSP directive takes precedence and the legacy header is ignored on that browser.
This is exactly why the allow-list ability matters: if you need to let one specific external site embed your pages, frame-ancestors is the only header that can do it correctly. You can also fold the directive into a larger CSP that you are already sending, rather than adding a separate header.
Should I send both headers?
The pragmatic recommendation is to send both headers. Modern browsers will honor frame-ancestors and ignore X-Frame-Options; older browsers that do not understand frame-ancestors will fall back to X-Frame-Options. Together they cover the whole range of clients.
The critical rule is that the two headers must express the same intent. If you want to forbid framing entirely, pair frame-ancestors 'none' with X-Frame-Options: DENY. If you want same-origin only, pair frame-ancestors 'self' with X-Frame-Options: SAMEORIGIN:
# block all framing, every browser:
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY
# same-origin framing only:
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGINNote the one case the two headers cannot agree on: an external allow-list. If you need specific third-party origins to frame you, set frame-ancestors with those origins and simply omit X-Frame-Options — there is no XFO value that expresses "allow these particular other sites," and supplying a misleading one only causes confusion.
Common gotcha: X-Frame-Options: ALLOW-FROM https://example.com is deprecated and is ignored by modern browsers. It was never reliably supported and you should not depend on it. If you need to allow specific origins to frame your pages, you MUST use CSP frame-ancestors with an explicit origin list, not X-Frame-Options. Equally, sending X-Frame-Options and frame-ancestors with conflicting values produces inconsistent behavior across browsers — some honor one, some the other — so always keep the two headers consistent with each other.
How do I verify it works?
Verification is straightforward because both headers are visible in any response. Open your browser's developer tools, load the page, and inspect the response headers for the document request — you should see Content-Security-Policy containing a frame-ancestors directive and, ideally, an X-Frame-Options header alongside it. From a terminal you can do the same with a single request:
curl -sI https://example.com | grep -iE 'frame-ancestors|x-frame-options'Test the actual behavior
Beyond reading the headers, confirm the behavior: create a tiny local HTML file with an <iframe src="https://your-site.example"> and open it. If your headers are working, the frame should stay blank and the browser console should log a refusal to display the page in a frame. Remember to check the pages that actually matter — settings, account, and transactional pages — not just your homepage, since those are the pages an attacker would want to frame.
Scan any URL to see whether it sends X-Frame-Options and a CSP frame-ancestors directive — and whether the two are consistent.
Check your security headers →Related reading
For the full picture of how these headers fit together, see the pillar guide Security Headers: the complete guide. To go deeper on Content-Security-Policy beyond frame-ancestors, read the Content-Security-Policy guide.