X-Frame-Options
An attacker puts your page inside an iframe on their own site, makes it invisible, and lines up a button of theirs underneath a button of yours. The user clicks "claim prize." The browser actually delivers the click to your page, transferring money or revoking permissions or whatever the hidden button does.
That is clickjacking. X-Frame-Options is the oldest defence: a response header that tells the browser which origins are allowed to put your page in a frame.
The attack
A transparent iframe overlays a visible button. The click lands inside the iframe.
X-Frame-Options: DENY in the response, refuses to render it. The invisible hitbox is simply empty. Nothing about this is exotic. The technique was documented in 2008, and enough high-value buttons (transfer, delete, share, revoke) still live on pages with no frame protection that clickjacking remains a working attack today.
The defence
Send one response header. The browser honours it before rendering inside any frame.
X-Frame-Options: DENYWhen a page embeds your URL in a frame, the browser fetches the HTML, checks this header, and refuses to render if the value says so. The frame stays empty. The attacker cannot catch the click because there is nothing to click.
Defined in RFC 7034. The spec is Informational; the de-facto standard is what Chrome, Firefox, and Safari have agreed to implement.
The three values
ALLOW-FROM was part of the original spec. No modern browser implements it: if your policy is "only example.com may frame me," ship CSP frame-ancestors instead. XFO with ALLOW-FROM is equivalent to no header at all in practice.
Modern replacement
CSP's frame-ancestors directive does what XFO does, with three advantages: it supports multiple origins, it supports wildcards, and it is not plagued by the ALLOW-FROM fragmentation.
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://partner.example.comWhen both headers are present and they disagree, browsers defer to CSP per the CSP Level 2 and 3 specs. The usual pattern in 2026 is to ship both: XFO for the long tail of older browsers that might still be out there, CSP for everyone else.
When framing is legitimate
Some pages actually want to be framed. Embeddable widgets (video players, payment widgets, OAuth consent screens, SSO interstitials) are designed to live inside another page. For those, set the allowlist precisely:
Content-Security-Policy: frame-ancestors 'self' https://checkout-partner.example.comNever use SAMEORIGIN on a page that needs cross-origin embedding. It blocks the feature. Never use DENY either.
Common mistakes
Any origin can frame your pages. Worst case on authenticated actions (transfer, delete, admin toggle). Ship something.
Every modern browser ignores it. Behaves as "no protection." Use CSP frame-ancestors instead.
Browsers check HTTP headers, not the document. XFO from a meta tag is ignored.
SSO and payment screens are the usual victims. The page ships defaults for the app, nobody remembers to override them for the embeddable route, the vendor integration silently breaks.
Inconsistent across edge configs and origin servers. Decide on one source of truth, set the other from it.
Check whether your pages ship XFO, whether they also ship CSP frame-ancestors, and whether the two agree: scan your domain.