Checked on 2026-08-24.
X-Frame-Options gave sites a switch with two positions: nobody, or only me. frame-ancestors gave them a guest list. It's a directive inside a Content Security Policy rather than a header of its own, it's specified in CSP Level 2 and Level 3, and where both exist it's the one the browser actually obeys.
The guest list is the whole point. A site can name the partner that's allowed to embed its dashboard, or its own desktop app's custom URL scheme, or a wildcard covering its staging subdomains, and refuse everything else. That flexibility is also where it goes wrong, because a source list has four or five ways to look correct and match nothing.
The syntax
The value is either the single token 'none' or a space-separated list of source expressions. The quotes on 'none' are not decoration; the specification requires them, and frame-ancestors none without them is a hostname, not a keyword.
For this directive the useful source expressions are:
'none'— no frame, from anywhere. The closest equivalent toX-Frame-Options: DENY.'self'— the page's own origin, exactly. Not "my company", not "my domain": the same scheme, host, and port, with a narrow allowance for an upgrade fromhttptohttps.www.example.comandexample.comare two origins, and'self'on one does not cover the other.- A scheme, written with its colon, like
https:. Any origin on that scheme matches. - A host, like
https://partner.example.com, optionally with a port. - A wildcard host, like
*.example.com, matching subdomains.
Unlike most CSP directives, this one governs more than <iframe>. MDN lists <frame>, <iframe>, <object>, and <embed> — the whole family of elements that can put one document inside another.
Here's what real lists look like. Every value below came from a request we made on 2026-08-24:
| Site | frame-ancestors |
|---|---|
| GitHub | 'none' |
| Fastmail | 'self' |
| Sentry | *.sentry.io |
| Proton Mail | https://calendar.proton.me https://drive.proton.me |
| Notion | 'self' https://app.notion.com notion://app.notion.com notion://www.notion.so https://app.contentful.com |
Notion's is the one to study, because every entry earns its place — and because reading it correctly means knowing which host sent it. We requested https://www.notion.so, and the response carrying this policy came back from https://www.notion.com, after a redirect. So 'self' here means www.notion.com framing itself, not the .so address we typed. https://app.notion.com is a second web origin on a different host, which 'self' would not have covered. The two notion:// entries look like the desktop application, which loads pages through its own URL scheme — and note that www.notion.so appears in this list only there, under the custom scheme, never as an HTTPS origin. https://app.contentful.com is a third party: a content management system whose editors evidently need Notion pages in a frame. Five entries, four distinct reasons, and one host that isn't the one we asked for.
Proton Mail's is interesting for the opposite reason: the list names Proton Calendar and Proton Drive, and does not include 'self'. Read literally, Proton Mail declines to frame itself while allowing its two sibling apps to do it.
It beats X-Frame-Options
When a response carries both, the directive wins. This isn't a browser quirk or a race between headers; it's written into the specification. CSP Level 3 states that "the frame-ancestors directive overrides the X-Frame-Options header," and that when an enforced policy contains the directive, the header "will be ignored." The HTML Standard's algorithm implements exactly that: it checks the policy list first, and returns "allowed" without ever looking at X-Frame-Options as soon as it finds an enforced frame-ancestors.
The word enforced is doing real work in that sentence. A policy delivered in Content-Security-Policy-Report-Only has a different disposition, and the algorithm skips it. A report-only frame-ancestors therefore neither blocks a frame nor suppresses your X-Frame-Options — which is exactly what you want while you're testing, and a trap if you thought report-only mode was live.
The practical consequence is that the header in your terminal output may not be the one deciding anything. Two measured examples make it concrete:
- Evernote sends
X-Frame-Options: DENYtogether withframe-ancestors https://docs.google.com https://www.evernote.com https://evernote.com. Those two instructions flatly contradict each other. A current browser obeys the list and ignores theDENY, so Google Docs is permitted to frame Evernote and your page is not. - Fastmail sends
X-Frame-Options: DENYwithframe-ancestors 'self'. Same shape, smaller list: theDENYwould have stopped Fastmail framing itself, and the directive overrides it and allows exactly that.
Neither site is misconfigured. Both are sending a legacy header for old clients and a real policy for everything else. You just have to read them in the right order. X-Frame-Options explained covers the legacy half.
One honest caveat about the measurements, since it's the same trap: our own Gmail row shows an X-Frame-Options value and no frame-ancestors at all, because an unauthenticated request never reaches the mailbox. It lands on Google's sign-in page, and that's whose headers we captured. Why Gmail and Google Calendar never load in an iframe follows the whole redirect chain.
