X-Frame-Options vs CSP frame-ancestors: which clickjacking header wins when you send both
How the two headers that stop your pages being framed differ, per CSP Level 3 and the HTML Standard: which one wins when both are sent, why ALLOW-FROM is ignored, and why a meta tag won't work.
If another site can load your page in an <iframe> and lay a transparent button over it, a visitor can click a button on your page without knowing it. That’s clickjacking; the specs call it UI redressing. The defence is a response header telling the browser not to display the response inside another page, and there are two for the job: X-Frame-Options and the frame-ancestors directive of Content Security Policy.
X-Frame-Options has two values
X-Frame-Options was written up in 2013 as RFC 7034, an Informational RFC. Its processing rules now live in section 7.7 of the WHATWG HTML Standard, which says its definition supersedes the RFC.
Two values are valid:
DENY: never display the document inside another page.SAMEORIGIN: walk outwards through every document that contains this one; if any of them has a different origin, block.
The RFC also described ALLOW-FROM https://example.com. The HTML Standard says plainly that it is not to be implemented. Sent on its own it counts as an invalid value, which is processed exactly as if the header were absent. In that case, X-Frame-Options imposes no framing restriction.
Multiple values have rules too. If there is more than one value and any of them is deny, sameorigin or allowall, framing is blocked. If every value is invalid, the header is treated as absent. One comma-separated header and several separate headers give the same result.
frame-ancestors takes a source list
In CSP Level 3, frame-ancestors restricts which URLs may embed the resource with frame, iframe, object or embed. The value is 'none', 'self', or a space-separated source list of schemes such as https: and hosts such as https://partner.example.
The browser walks outwards through the containing documents and checks whether each ancestor’s origin, parsed as a URL, matches that source list. If any one of them doesn’t match, it blocks. Matching follows CSP’s source-expression rules for scheme, host and port rather than a plain origin comparison.
The CSP spec describes 'none' as roughly equivalent to DENY and 'self' to SAMEORIGIN. They aren’t identical: 'self', for example, also matches the https version of the same host when the ports match or each is the default for its scheme. What X-Frame-Options can’t express, such as “our own site plus one partner”, is a one-line list here.
Two things catch people out:
frame-ancestorsdoes not fall back todefault-src.default-src 'none'alone imposes noframe-ancestorsrestriction; if this policy has to control embedding, writeframe-ancestorsexplicitly.frame-ancestorsin a<meta http-equiv="Content-Security-Policy">tag is ignored. RFC 7034 section 4 likewise requiresX-Frame-Optionsto be sent as an HTTP header and says it is ignored when declared in a meta tag.
Sending both
Under the current HTML processing algorithm, CSP is checked first. If an enforced policy contains frame-ancestors, X-Frame-Options is skipped without being read. The CSP spec gives the same precedence and says it exists to allow backwards-compatible deployment.
A frame-ancestors directive sent in Content-Security-Policy-Report-Only is not enforced, so in that case X-Frame-Options still decides. While you trial a new policy in report-only mode, the old header keeps protecting the page.
You can send both headers:
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
A browser that enforces frame-ancestors decides by the first line and ignores the second. A client that doesn’t enforce frame-ancestors and consults X-Frame-Options falls back to the second line.
To allow a partner, add it to the first line ('self' https://partner.example). X-Frame-Options has no valid value that names a host, though, so the two headers can’t express the same allowlist. With the second line left at SAMEORIGIN, browsers enforcing the first line let the partner frame the page, while a client deciding by X-Frame-Options blocks it.
Don’t replace the fallback header with ALLOW-FROM. Where X-Frame-Options is consulted, that value supplies no protection. Browsers enforcing the first line keep using frame-ancestors.
motitory.com configures these two headers for every static response in public/_headers.
What these headers don’t do
Both headers answer one question: may this document be shown inside another document? A top-level page load isn’t checked at all, and nothing here affects scripts, images or anything else covered by other CSP directives.
The header has to be present on each document response. Check your browser’s network panel or use curl -I to make sure no route is missing it.