Framing and UI redress
Clickjacking and frame-ancestors
Clickjacking is your page, loaded in an invisible frame over something the visitor wanted to click. Their session, their cookies, their click — aimed at a button they cannot see.
frame-ancestors is the directive that says who may embed you, and it is the one CSP directive whose violation reports tell you about somebody else's site rather than your own.
What the attack is
An attacker's page loads yours in an <iframe>, sets it to near-zero opacity, and positions the frame so that a particular control in your interface sits exactly under something enticing in theirs. The visitor clicks what they can see. The browser delivers the click to what is actually on top — your page, with the visitor's session attached.
Nothing is injected and nothing is intercepted. Every security control on your side behaves normally, because from the browser's point of view the user really did click your button. The attack is entirely in the framing.
The defence is correspondingly simple: refuse to be framed by anyone you did not name. That is one header, and on most sites the right value is frame-ancestors 'none'.
Why frame-ancestors supersedes X-Frame-Options
Four reasons, of which the third is the one this site cares about.
- It can name more than one origin
- X-Frame-Options had DENY and SAMEORIGIN, and an ALLOW-FROM that Chrome and Safari never implemented — so a site with one partner who legitimately embedded it had no working option at all. frame-ancestors takes a source list: several origins, schemes, wildcards.
- It checks the whole ancestor chain
- A frame inside a frame inside a page is three ancestors, and frame-ancestors is evaluated against all of them. X-Frame-Options' handling of nested chains varied between browsers, which made SAMEORIGIN mean subtly different things depending on who was looking.
- It reports, and X-Frame-Options does not
- This is the difference that matters here. X-Frame-Options blocks silently: the frame stays blank and you never learn it was attempted. frame-ancestors produces a violation report, so being framed is something you can see rather than something you assume is not happening.
- Where both are present, the enforcing one wins
- A browser that supports frame-ancestors ignores X-Frame-Options when an enforcing policy sets both. A report-only policy does not override the old header, which is exactly what you want while you are still reading reports: keep X-Frame-Options doing the blocking, and let frame-ancestors tell you who is knocking.
What the report looks like
Read the two URL fields carefully — they are the other way round from every other directive.
{
"csp-report": {
"document-uri": "https://app.example.com/settings/transfer",
"referrer": "",
"violated-directive": "frame-ancestors",
"effective-directive": "frame-ancestors",
"original-policy": "default-src 'self'; frame-ancestors 'self'; report-uri https://ingest.headerhawk.com/r/abc123",
"disposition": "report",
"blocked-uri": "https://free-prizes.example/",
"status-code": 200,
"script-sample": ""
}
}How it appears in HeaderHawk
frame-ancestors blocking free-prizes.example
- The report comes from your document — the one that refused to be framed — so document-uri is your page and blocked-uri is the would-be embedder. Everywhere else in CSP, blocked-uri is a resource you tried to load.
- That makes this the one report that tells you about somebody else's site. A frame-ancestors issue is either a partner you forgot to allow or somebody who should not be embedding you, and the URL is usually enough to tell which.
- Browsers differ in how much of the ancestor they disclose, and some send an empty value. HeaderHawk records an empty blocked URI as the host unknown, so an issue titled 'frame-ancestors blocking unknown' means the browser withheld the embedder, not that there wasn't one.
- frame-ancestors is header-only. A policy delivered in a <meta> tag has this directive ignored, so a page set up that way produces no reports and has no protection — and looks fine.
- disposition: report means the framing succeeded. Report-only tells you it happened; enforcing is what makes the frame stay blank.
Field by field: the frame-ancestors reference.
What HeaderHawk does with it
Framing reports are low-volume and high-interest, which is the opposite of most CSP traffic. Each embedder becomes one issue —frame-ancestors plus the host that tried — carrying a report count, the pages of yours that were framed, and first- and last-seen timestamps. The page list is the part worth reading: a single page framed repeatedly is targeted, and which page it is tells you what the attacker wants clicked.
Because the volume is low, the confidence rules matter. A window reported by two or more browser families alerts immediately; a handful of reports from one browser goes into a daily or weekly digest instead. Both are the correct outcome here — a single report from one browser is as likely to be a researcher, a translation proxy or an embedded preview as an attack, and a digest is the right place to see it without being woken.
Drilling into a report gives the embedder URL as sent, the page of yours that refused, the full policy in force, and the browser and operating system. If you are deciding whether to add a partner to the allowlist, that is the evidence; if you are deciding whether to worry, the embedder's domain usually settles it in a second.
Where CSP alone is not enough
Framing reports tell you about attempts that happened. They tell you nothing about whether you are protected — and that gap is wider here than on any other page in this section.
- There is no HTTP response header monitoring. This is the limit that matters most on this page. Nothing in HeaderHawk fetches your site or compares the headers you serve over time, so it cannot tell you that frame-ancestors went missing from a route, or that a CDN rule stopped passing it through. Reports going quiet and protection going missing look identical from here.
- A report only exists if your page was framed. Silence means nobody framed you, nobody framed you in a browser that reports, or your page is not sending the directive at all — and the three are indistinguishable.
- A phishing page that copies your interface never touches your origin. frame-ancestors governs embedding; it has nothing to say about a convincing replica on somebody else's domain.
- frame-ancestors does not fall back to default-src. A policy of default-src 'none' with no frame-ancestors is embeddable by anyone, which is exactly the configuration clickjacking needs, and it is easy to read that policy as tight.
- It is ignored in a <meta> policy. If your CSP is set in HTML rather than an HTTP header, the directive does nothing and reports nothing — and the rest of your policy still works, so nothing looks wrong.
- Some browsers withhold the embedder's URL. The blocked URI arrives empty, which HeaderHawk records as the host unknown. You learn that you were framed without learning by whom.
- Report-only mode prevents nothing. The framing happened; you got a report about it. Only an enforcing policy in your own header stops it.
- Framing is not the whole of clickjacking defence. SameSite cookies, re-authentication on sensitive actions, and not putting irreversible operations one click away are all outside CSP's reach.
Where to go next
- The
frame-ancestorsreference has the syntax, the source expressions that are and are not valid, and the browser support. frame-srcis the mirror image: what your page is allowed to embed, rather than who may embed it.- CSP and XSS covers the directive most people reach for first, and what it does not reach.
Find out who is framing you
Add frame-ancestors in report-only mode and watch for a fortnight before you enforce it. Partners you forgot about show up first.