Skip to content

Conversation

@zcorpan
Copy link
Member

@zcorpan zcorpan commented Aug 14, 2025

This aligns ancestorOrigins exposure with referrer policy when using the iframe referrerpolicy attribute, so an embedder can prevent revealing its own origin to embedded documents. If an <iframe> uses referrerpolicy="no-referrer" or same-origin (and the parent and child are cross-origin), the parent’s origin and any same-origin ancestors are replaced with opaque origins (until reaching an ancestor that is cross-origin). Other policies continue to expose full origins. If there's no referrerpolicy attribute, the embedder document's referrer policy is used.

This approach keeps existing behavior by default (for web compat) while addressing privacy concerns with an opt-out.

The algorithm reuses the parent's existing list of ancestor origins, avoiding synchronous cross-process lookups and ensuring a stable snapshot even if ancestors mutate their referrerpolicy attributes later.

Fixes #1918. Closes #2480.

(See WHATWG Working Mode: Changes for more details.)


/browsers.html ( diff )
/browsing-the-web.html ( diff )
/document-lifecycle.html ( diff )
/document-sequences.html ( diff )
/dom.html ( diff )
/iframe-embed-object.html ( diff )
/index.html ( diff )
/infrastructure.html ( diff )
/nav-history-apis.html ( diff )

@zcorpan
Copy link
Member Author

zcorpan commented Aug 14, 2025

This is the same as @annevk's #2480 but applied on top of the latest main. Rebasing involved a lot of unrelated conflicts, so instead I copied over the content in a new branch.

@annevk
Copy link
Member

annevk commented Aug 14, 2025

I think using the document's referrer is the wrong model. Because we don't care about the referrer of top-level example.com (how the user got to example.com, perhaps from search.example), we care about example.com's policy (or its iframe's policy) when embedding widget.example, when determining what information widget.example should have access to.

@zcorpan
Copy link
Member Author

zcorpan commented Aug 14, 2025

I created a doc to work out how to specify this with the new model: https://docs.google.com/document/d/1TDryRMiw7sVKBfrvnzEQk0PzGQk7_G7ShF4Uy68MvSU/edit?usp=sharing

Copy of the doc's current state

Example 1

  • example.org (no referrer policy)
    • Iframe referrerpolicy=”no-referrer”
      • Widget.example

Result: [“https://widget.example”, “null”]

Example 2

  • example.org (no referrer policy)
    • Iframe referrerpolicy=”no-referrer”
      • Widget.example and navigates to other.example

Result: ???

Algo

HTML:

  1. Let output be a new list of strings.
  2. Let currentDoc be the Location object's relevant Document.
  3. Let innerDoc be current.
  4. Let currentContainer be null.
  5. While currentDoc’'s container document is non-null:
    1. Set currentContainer to current’s node navigable’s container.
    2. Set currentDoc to current's container document.
    3. Let origin be the result of getting an origin if the referrer policy allows given currentContainer, currentDoc, and innerDoc.
    4. Append the serialization of current's origin to output.
  6. Return output.

Referrer Policy:

To get an origin if the referrer policy allows given an element or null container, a document doc, and a document innerDoc:

  1. Let elementReferrerPolicy be the empty string.
  2. If element is not null, then set elementReferrerPolicy to element’s referrerpolicy attribute’s state.
  3. Let docReferrerPolicy be doc’s referrer policy.
  4. Let referrerPolicy be elementReferrerPolicy.
  5. If referrerPolicy is the empty string, then set referrerPolicy to docReferrerPolicy.
  6. If referrerPolicy is “no-referrer”, then return “null”.
  7. If referrerPolicy is “same-origin” and doc’s origin is not same origin with innerDoc’s origin, then return “null”.
  8. If referrerPolicy is “strict-origin”, "strict-origin-when-cross-origin", “no-referrer-when-downgrade”, and innerDoc’s URL is not potentially trustworthy but doc’s URL is potentially trustworthy, then return “null”.
  9. Return the serialization of doc’s origin.

@zcorpan
Copy link
Member Author

zcorpan commented Sep 1, 2025

Doc updated. I've changed the algorithm to avoid exposing A's origin in the A->A->C case with the innermost iframe having referrerpolicy="no-referrer". Also added examples from w3c/webappsec-referrer-policy#77 (comment) and a polyfill to test with.

Copy of the doc's current state

Algorithm

HTML:

  1. Let output be a new list of strings.
  2. Let currentDoc be the Location object's relevant Document.
  3. Let innerDoc be currentDoc.
  4. Let currentContainer be null.
  5. Let prevOrigin and prevMasked be uninitialized.
  6. While currentDoc’s container document is non-null:
    1. Set currentContainer to currentDoc’s node navigable’s container.
    2. Set currentDoc to currentDoc's container document.
    3. Let « origin, masked » be the result of [=getting an ancestor origin if the referrer policy allows=] given currentContainer, currentDoc, and innerDoc.
    4. If prevMasked is true and origin is same origin with prevOrigin, then set masked to true.
    5. If masked is true, then append “null” to output.
    6. Otherwise, append the serialization of origin to output.
    7. Set prevOrigin to origin.
    8. Set prevMasked to masked.
  7. Return output.

Referrer Policy:

To get an ancestor origin if the referrer policy allows given an Element container, a Document doc, and a Document innerDoc:

  1. Let elementReferrerPolicy be the empty string.
  2. If container is an iframe element, then set elementReferrerPolicy to container’s referrerpolicy attribute’s state.
  3. Let docReferrerPolicy be doc’s policy container’s referrer policy.
  4. Let referrerPolicy be elementReferrerPolicy.
  5. If referrerPolicy is the empty string, then set referrerPolicy to docReferrerPolicy.
  6. Let masked be false.
  7. If referrerPolicy is “no-referrer”, then set masked to true.
  8. Otherwise, if referrerPolicy is “same-origin” and doc’s origin is not same origin with innerDoc’s origin, then set masked to true.
  9. Return « doc’s origin, masked ».

Note: Since mixed content checks prevent non-secure context documents in secure context documents, there’s no need to check secure context for “strict-origin”, "strict-origin-when-cross-origin", and “no-referrer-when-downgrade”.

Polyfill + demo

https://software.hixie.ch/utilities/js/live-dom-viewer/saved/14030

Examples

iframe referrerpolicy=”no-referrer”

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • iframe referrerpolicy=”no-referrer”, origin: https://b.com

Child: ["null"]

Examples below are from w3c/webappsec-referrer-policy#77 (comment)

top -> sandboxed iframe -> 3rd party iframe (ad)

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • Iframe sandbox, origin: opaque
      • Iframe, origin: https://b.com

Grandchild: ["null","https://a.com"]
Child: ["https://a.com"]

{1, a.com} default referrer policy -> loads {2, b.com} with noreferrer attribute on the iframe tag that's loading b.com -> loads {3, a.com}

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • iframe referrerpolicy=”no-referrer”, origin: https://b.com
      • iframe, origin: https://a.com

Grandchild: ["https://b.com","null"]
Child: ["null"]

{1, a.com} default referrer policy -> loads {2, b.com} with noreferrer attribute on the iframe tag inside {2} -> loads {3, a.com}

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • iframe, origin: https://b.com
      • Iframe referrerpolicy=”no-referrer”, origin: https://a.com

Grandchild: ["null","https://a.com"]
Child: ["https://a.com"]

{1, a.com} default referrer policy -> loads {2, a.com} with noreferrer attribute on the iframe tag inside {2} -> loads {3, c.com}

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • iframe, origin: https://a.com
      • iframe referrerpolicy=”no-referrer”, origin: https://c.com

Grandchild: ["null","null"]
Child: ["https://a.com"]

{1, a.com} default referrer policy -> loads {2, b.com} with default referrer policy -> loads {3, b.com} with noreferrer attribute on the iframe tag inside {3} -> loads {4, a.com}

  • Top-level browsing context, default referrer policy, origin: https://a.com
    • iframe, origin: https://b.com
      • iframe, origin: https://b.com
        • iframe referrerpolicy=”no-referrer”, origin: https://a.com

Grandgrandchild: ["null","null","https://a.com"]
Grandchild: ["https://b.com","https://a.com"]
Child: ["https://a.com"]

@zcorpan
Copy link
Member Author

zcorpan commented Sep 2, 2025

After discussing with @farre we found out that reading the iframe's referrerpolicy attribute every time ancestorOrigins is read is not great. Instead, the value of the referrerpolicy attribute should be read when the document is created and stored on the document (or in its policy container).

@zcorpan zcorpan changed the title Redact ancestorOrigins using "the document's referrer" Redact ancestorOrigins using referrer policy Sep 3, 2025
@zcorpan
Copy link
Member Author

zcorpan commented Sep 5, 2025

reading the iframe's referrerpolicy attribute every time ancestorOrigins is read is not great.

This was not correct, the spec computes the ancestor origins list when the Location object is created, which happens when the Window object is created.

When a Location object is created, its ancestor origins list must be set to a DOMStringList object whose associated list is the list of strings that the following steps would produce:

https://html.spec.whatwg.org/#concept-location-ancestor-origins-list

Each Window object is associated with a unique instance of a Location object, allocated when the Window object is created.

https://html.spec.whatwg.org/#the-location-interface

So no need to store iframe's referrerpolicy. However, we want to minimize IPC and avoid inconsistency e.g. when an ancestor iframe's referrerpolicy attribute is mutated and a new child iframe is inserted (or is navigated). So the model we came up with now is to take the parent's ancestor origins list, mask some values as appropriate, and insert a new value.

Copy of the doc's current state

Algorithm

A Location object has an associated ancestor origin objects list. When a Location object is created, its ancestor origins list must be set to the list of origins that the following steps would produce:

  1. Let output be a new list of origins.
  2. Let innerDoc be the Location object's relevant Document.
  3. Let parentDoc be innerDoc’s container document.
  4. If parentDoc is non-null:
    1. Assert: parentDoc is fully active.
    2. Let parentLocation be parentDoc’s relevant global object’s Location object.
    3. Let ancestorOrigins be parentLocation’s ancestor origins list’s associated list.
    4. Let container be innerDoc’s node navigable’s container.
    5. Let referrerpolicyAttribute be the empty string.
    6. If container supports a referrer policy attribute (e.g., container is an iframe element), then set referrerpolicyAttribute to container’s referrer policy attribute’s state.
    7. Let docReferrerPolicy be parentDoc’s policy container’s referrer policy.
    8. Let referrerPolicy be referrerpolicyAttribute.
    9. If referrerPolicy is the empty string, then set referrerPolicy to docReferrerPolicy.
    10. Let masked be false.
    11. If referrerPolicy is “no-referrer”, then set masked to true.
    12. Otherwise, if referrerPolicy is “same-origin” and parentDoc’s origin is not same origin with innerDoc’s origin, then set masked to true.
    13. If masked is true, then append “null” to output.
    14. Otherwise, append parentDoc’s origin to output.
    15. For each ancestorOrigin in ancestorOrigins:
      1. If masked is true:
        1. If ancestorOrigin is same origin with parentDoc’s origin, then append a new opaque origin to output.
        2. Otherwise, append ancestorOrigin to output and set masked to false.
      2. Otherwise, append ancestorOrigin to output.
  5. Return output.

Note: Since mixed content checks prevent non-secure context documents in secure context documents, there’s no need to check secure context for “strict-origin”, "strict-origin-when-cross-origin", and “no-referrer-when-downgrade”.

A Location object has an associated ancestor origins list. When a Location object is created, its ancestor origins list must be set to a DOMStringList object whose associated list is the list of strings the following steps would produce:

  1. Let ancestorOrigins be this’s ancestor origin objects list.
  2. Let output be a new list of strings.
  3. For each origin in ancestorOrigins:
    1. Append the serialization of origin to output.
  4. Return output.

Polyfill + demo

https://software.hixie.ch/utilities/js/live-dom-viewer/saved/14047

Examples

iframe referrerpolicy=”no-referrer”

  • Top-level browsing context, default referrer policy, origin: https://a.com

Child: ["null"]

{1, a.com} default referrer policy -> loads {2, b.com} -> loads {3, a.com}

Grandchild: ["https://b.com","https://a.com"\]
Child: ["https://a.com"\]

Examples below are from w3c/webappsec-referrer-policy#77 (comment)

top -> sandboxed iframe -> 3rd party iframe (ad)

  • Top-level browsing context, default referrer policy, origin: https://a.com

Grandchild: ["null","https://a.com"\]
Child: ["https://a.com"\]

{1, a.com} default referrer policy -> loads {2, b.com} with noreferrer attribute on the iframe tag that's loading b.com -> loads {3, a.com}

Grandchild: ["https://b.com","null"\]
Child: ["null"]

{1, a.com} default referrer policy -> loads {2, b.com} with noreferrer attribute on the iframe tag inside {2} -> loads {3, a.com}

Grandchild: ["null","https://a.com"\]
Child: ["https://a.com"\]

{1, a.com} default referrer policy -> loads {2, a.com} with noreferrer attribute on the iframe tag inside {2} -> loads {3, c.com}

Grandchild: ["null","null"]
Child: ["https://a.com"\]

{1, a.com} default referrer policy -> loads {2, b.com} with default referrer policy -> loads {3, b.com} with noreferrer attribute on the iframe tag inside {3} -> loads {4, a.com}

Grandgrandchild: ["null","null","https://a.com"\]
Grandchild: ["https://b.com","https://a.com"\]
Child: ["https://a.com"\]

TODOs

@zcorpan
Copy link
Member Author

zcorpan commented Sep 8, 2025

I've updated this PR, the algorithm should be the same as in the doc.

@zcorpan zcorpan added the agenda+ To be discussed at a triage meeting label Sep 25, 2025
@zcorpan
Copy link
Member Author

zcorpan commented Oct 8, 2025

I wrote a summary of what this change does in the OP. (To be used in the commit message when squashing.)

@noamr
Copy link
Collaborator

noamr commented Oct 13, 2025

cc @domfarolino

@zcorpan zcorpan removed the agenda+ To be discussed at a triage meeting label Oct 23, 2025
@annevk
Copy link
Member

annevk commented Oct 23, 2025

Before I forget: we need to patch https://w3c.github.io/ServiceWorker/#client-ancestororigins at the same time.

This aligns `ancestorOrigins` exposure with referrer policy, so an embedder can prevent revealing its own origin to embedded documents. If an `<iframe>` uses `referrerpolicy="no-referrer"` or `same-origin` (and the parent and child are cross-origin), the parent’s origin and any same-origin ancestors are replaced with opaque origins (until reaching an ancestor that is cross-origin). Other policies continue to expose full origins. If there's no `referrerpolicy` attribute, the embedder document's referrer policy is used.

This approach keeps existing behavior by default (for web compat) while addressing privacy concerns with an opt-out.

The algorithm reuses the parent's existing list of ancestor origins, avoiding synchronous cross-process lookups and ensuring a stable snapshot even if ancestors mutate their `referrerpolicy` attributes later.

Fixes #1918. Closes #2480.
@zcorpan zcorpan force-pushed the zcorpan/redact-ancestororigins branch from 3c5e61f to 0004ccb Compare October 27, 2025 21:50
@zcorpan
Copy link
Member Author

zcorpan commented Oct 27, 2025

@annevk as far as I can tell, the call sites for Create Window Client use a Location object's ancestor origins list, which is still defined and compatible with this PR. But we should have tests for it.

@johannhof
Copy link
Member

What is the reason for using the referrer policy instead of a new attribute / header? Are we not concerned that this would lead to situations where one of the two features is required, making it impossible for sites to opt out of the other?

@zcorpan
Copy link
Member Author

zcorpan commented Dec 10, 2025

Additionally, on https://software.hixie.ch/utilities/js/live-dom-viewer/?saved=14376 with safari, uncommenting //w(loc.ancestorOrigins.length); should gives us the log 2, 2, 0, but since Safari caches the object, after the navigation, it still returns 2, which is contrary to what the spec says in step 1, which means that SameObject can't hold for all use cases (and therefore should be removed).

I think this means new object every time makes more sense.

This PR and the tests should be ready now.

source Outdated
Comment on lines 96926 to 96927
<li><p>Let <var>innerDoc</var> be <var>location</var>'s <span>relevant
<code>Document</code></span>.</p></li>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a Location object is created, a Document does not yet exist, so relevant Document will be null here. Solution: run these steps after a Document is created.

source Outdated
Comment on lines 96740 to 96743
<li><p>Set <var>location</var>'s <span
data-x="concept-internal-location-ancestor-origin-objects-list">internal ancestor origin objects
list</span> to the result of running the <span>internal ancestor origin objects list creation
steps</span> given <var>location</var>.</p></li>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running these steps only when a Location object is created means that the list is not updated when navigating an iframe from initial about:blank to something else, if the referrerpolicy attribute was mutated in between (before navigation start).

Solution: run these steps when a Document is created, and store the list on Document.

This fixes the fact that "relevant Document" is not yet set when a Location is created, and that the list should be updated when navigating from initial about:blank (where the Window and Location is the same but a new Document is created).
data-x="environment">environments</span>, there's no need to check <span>secure context</span>
for "<code data-x="">strict-origin</code>", "<code
data-x="">strict-origin-when-cross-origin</code>", and "<code
data-x="">no-referrer-when-downgrade</code>".</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should pile onto this by mentioning that we also don't need to check for origin-when-cross-origin, since that would only restrict the referrer to the origin, which is already the most amount of information that ancestorOrigins exposes. It's kind of obvious, but I find myself thinking through it every time I review this PR heh.

<h4><code>iframe</code> referrer policy</h4>

<div algorithm>
<p>To <dfn data-x="determining the iframe referrer policy">determine the <code>iframe</code> referrer policy</dfn> given null or an element <var>embedder</var>:</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I slightly prefer the pattern element-or-null

<var>targetNavigable</var>'s <span data-x="nav-container">container</span>.</p>
params</span> with:</p>

<ul>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be consistent with source snapshot params above, and make this:

    <dl class="props">
     <dt><span data-x="target-snapshot-params-sandbox">sandboxing flags</span></dt>
     <dd>...</dd>

     <dt><span data-x="target-snapshot-params-iframe-referrer-policy"><code>iframe</code> referrer policy</span></dt>
     <dd>...</dd>

<dt><dfn data-x="navigation-params-sandboxing">final sandboxing flag set</dfn></dt>
<dd>a <span>sandboxing flag set</span> to impose on the new <code>Document</code></dd>

<dt><dfn data-x="navigation-params-iframe-referrer-policy"><code>iframe</code> referrer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm noticing that this is only set in a single place, and it's in the "response"-is-null path which is used for object/embed and x-mixed-replace, which means this line is technically doing nothing since it's never the iframe case.

I'd expect this member to also be assigned wherever else #navigation-params-sandboxing is set, like https://html.spec.whatwg.org/#populating-a-session-history-entry:navigation-params-sandboxing-2, but in particular, the usual case which is https://html.spec.whatwg.org/#populating-a-session-history-entry:navigation-params-sandboxing-3.

</li>

<li>
<p>If <var>embedder</var> is non-null, then:</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is solving the pre-existing problem that https://html.spec.whatwg.org/#the-location-interface:concept-location-ancestor-origins-list has, which is that RIGHT when it's created, the relevant document doesn't exist yet, right? Could I ask you to tackle that separately from this PR, just to minimize the moving pieces especially around new document creation.

<li><p>Set <var>window</var>'s <span data-x="concept-document-window">associated
<code>Document</code></span> to <var>document</var>.</p></li>

<li><p>Set <var>document</var>'s <span
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could get rid of this block too, just for now, if we tackle the whole "build up the Location object's ancestor origins stuff after the Document has been created" as a follow-up. I just think that'll simplify this PR up since it's already getting non-trivial.

</ol>
</div>

<p class="note">This is used by the <span>internal ancestor origin objects list creation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically true, but it's also used by a few other places. Why call this one out in particular?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

redact location.ancestorOrigins according to Referrer Policy

10 participants