Security Headers

Clickjacking & X-Frame-Options: stop your site being framed

What clickjacking actually is

Clickjacking is a trick of layering, not a breach of your server. An attacker loads your real, logged-in site inside an <iframe> on a page they control, then makes that frame transparent or hides it behind decoy content. The user sees the attacker's page and clicks what looks like a harmless button — but the click lands on your site underneath. If the user is authenticated, that click can do something real: confirm an action, change a setting, approve a request.

The defense is simple and one-time: tell the browser who, if anyone, is allowed to put your pages inside a frame.

The modern control: frame-ancestors

The current, preferred way is a directive inside your Content-Security-Policy header. To forbid framing entirely:

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

To allow only your own pages to frame your content (useful if you legitimately embed yourself):

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

And to allow a specific trusted partner:

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

frame-ancestors is the one to reach for first because it is part of CSP, supports an allowlist, and is honoured by current browsers.

The legacy fallback: X-Frame-Options

Before frame-ancestors, the header was X-Frame-Options. It is coarser — it does not support an allowlist of multiple origins — but it is still worth sending for older clients:

  • X-Frame-Options: DENY blocks all framing.
  • X-Frame-Options: SAMEORIGIN allows only your own origin to frame you.

Setting both frame-ancestors and X-Frame-Options is normal and sensible: modern browsers follow the CSP directive, older ones fall back to the legacy header. They do not conflict.

What value should you actually use?

For the vast majority of sites, the honest answer is do not allow framing at all. A typical marketing site, dashboard or checkout has no reason to be embedded by anyone, so frame-ancestors 'none' plus X-Frame-Options: DENY is the safe default. Only relax it if you have a real embedding use case — and then allowlist exactly the origins involved, nothing wildcard.

A quick word on what framing can do

It helps to picture the payload. Because the framed page is your real, authenticated site, the hijacked click does whatever a real click would: a Delete button, a Confirm payment, a Grant access toggle, a one-click subscribe. The attacker does not need your password or session — they borrow the session the browser already has and steer a single, well-placed click. That is why even ordinary action buttons are worth protecting, not just obviously sensitive ones.

Why it is so often missing

Frameworks do not set these headers for you. Unless someone deliberately added an anti-framing policy, your pages can be framed by anyone — which is why this shows up constantly in header checks. The fix is a single line of config, applied site-wide, and it costs you nothing in performance or compatibility for a site that is never meant to be embedded.

Check whether you can be framed

You cannot tell from the page whether your responses forbid framing — the header is invisible to the rendered site. An automated scan reads your actual responses and reports whether frame-ancestors or X-Frame-Options is present and correctly set. Scan your site and close the framing gap.

Related reading

FAQ

Should I use X-Frame-Options or frame-ancestors?
Use frame-ancestors in your Content-Security-Policy as the primary control, and keep X-Frame-Options as a fallback for older browsers. Modern browsers honour the CSP directive; the legacy header covers clients that do not.
What is the safest X-Frame-Options value?
DENY blocks all framing and is the safest default for most sites. Use SAMEORIGIN only if you legitimately embed your own pages, and use CSP frame-ancestors with an explicit allowlist if a trusted partner needs to frame you.
Does clickjacking mean my server was hacked?
No. Clickjacking happens entirely on the attacker's page by loading yours in a hidden frame — your server is not breached. The defence is instructing browsers to refuse framing, which is a header you send, not a server fix.