RoastMyOpsec

Safety

CSP vs X-Frame-Options for Clickjacking Protection

April 6, 2023

CSP vs X-Frame-Options is a choice between a modern framing policy and a legacy header that older browsers still understand. Content-Security-Policy frame-ancestors is the stronger, more precise control for new sites. X-Frame-Options still matters as a compatibility fallback. Most public marketing sites should ship both, with CSP as the source of truth and X-Frame-Options as DENY or SAMEORIGIN to match.

The practical difference

X-Frame-Options is a single-purpose header. DENY blocks all framing. SAMEORIGIN allows the same origin to frame the page. ALLOW-FROM exists in older docs but is not a reliable modern control.

Content-Security-Policy frame-ancestors can allow none, self, or an explicit origin list. It replaces the need for ALLOW-FROM and can live next to the rest of a CSP policy that also limits scripts, objects, and form targets.

RoastMyOpsec's free header audit reads the primary document response only — no payload injection, no fuzzing. Missing framing controls show up as a no-brainer finding because browsers forgive clickjacking more easily without them.

How to choose based on the site

Start with how the page is used. A brochure marketing site almost never needs to be framed. An embeddable widget, status board, or partner checkout flow might. The header decision follows the product decision, not the other way around.

If the site must never appear inside another origin's iframe, set CSP frame-ancestors 'none' and X-Frame-Options DENY. If same-site tools frame dashboards, use frame-ancestors 'self' and X-Frame-Options SAMEORIGIN. If a named partner origin must frame a specific route, list that origin in frame-ancestors and do not pretend X-Frame-Options can express the same allowlist.

  • Marketing homepage, blog, and docs: deny framing unless a real embed exists.
  • App shells that iframe themselves: SAMEORIGIN / 'self'.
  • Partner embeds: CSP allowlist on that route only; do not weaken the rest of the site.

Side-by-side comparison

The table below is a planning tool, not a claim that one header works for every stack. Confirm what your CDN, reverse proxy, and app framework actually emit on the HTML document.

Decision factorX-Frame-OptionsCSP frame-ancestorsTakeaway
PrecisionDENY or SAMEORIGIN in practicenone, self, or origin listCSP wins when a named partner must frame one route
Browser coverageVery old browsers still read itModern browsers prefer it and it overrides XFO when both existShip both until you know your traffic is modern
ScopeFraming onlyCan sit beside script-src, object-src, form-actionCSP is a policy surface; XFO is a single lock
Misconfig riskALLOW-FROM is a false friendA loose default-src can still leave other holesFraming is one check; XSS defense is another
Best first moveDENY on public marketing HTMLframe-ancestors 'none' on the same responsesMatch the two so CDNs do not disagree

When X-Frame-Options still wins

X-Frame-Options wins as a compatibility belt. Some older enterprise browsers and crawlers still honor it. It also documents intent in a header that security scanners and humans can read without parsing a full CSP.

It does not win as a substitute for CSP. If you only set XFO and skip frame-ancestors, you are betting that every relevant client still treats XFO as authoritative.

When CSP frame-ancestors wins

CSP wins when you need an allowlist, when you already maintain a CSP, and when you want one policy language for framing plus script control. It also wins on routes that must be framed by a specific HTTPS origin without opening the rest of the site.

The tradeoff is operational: CSP is easy to break with a tag manager, a chatbot widget, or an inline style. Start report-only if you are new to CSP, but do not leave framing unprotected while you iterate on script-src.

Common mistakes

The first mistake is setting X-Frame-Options on APIs and forgetting the HTML document. Clickjacking is a document problem.

The second mistake is allowing framing globally because one blog embed needed it. Scope the exception.

The third mistake is a CSP that never mentions frame-ancestors, then assuming default-src covers it. It does not. frame-ancestors is its own directive.

Check your public headers

Paste the URL you own into RoastMyOpsec for a free header audit. The scan reads the primary response, grades CSP quality signals, HSTS, framing controls, Referrer-Policy, nosniff, and Permissions-Policy, and leaves exploit payloads out of the picture.

If framing is missing, fix it at the edge so every HTML route inherits the policy. Then re-scan.

Free audit the URL you own

RoastMyOpsec is a defensive public-surface roast: headers, cookies, sensitive paths, and more — no exploit payloads. Start with the free audit, then open the vault if the blurred findings look expensive.

Free audit now

FAQ

Is CSP or X-Frame-Options better for clickjacking?
CSP frame-ancestors is the better modern control. Keep X-Frame-Options as a matching fallback (DENY or SAMEORIGIN) so older clients are not left open.
Does frame-ancestors replace X-Frame-Options?
In supporting browsers, CSP frame-ancestors is the framing authority when both are present. Shipping only one header still leaves gaps, so most public sites should send both until you know you do not need the legacy signal.
Should a marketing site use DENY or SAMEORIGIN?
Use DENY and frame-ancestors 'none' unless a same-origin tool actually frames the page. SAMEORIGIN is for first-party iframes, not for 'maybe someday' embeds.
Does RoastMyOpsec exploit clickjacking?
No. The audit is a consented read of public headers and responses. Private IPs and localhost are blocked. No exploit payloads.

Sources

Related guides