Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand storage-access-preserving navigations to include same-origin-initiated navigations, not just self-initiated. #197

Open
bvandersloot-mozilla opened this issue Mar 1, 2024 · 16 comments

Comments

@bvandersloot-mozilla
Copy link
Collaborator

We got this bug filed on Firefox: Bug 1876504 - With storage access granted, nested iframe is loaded without cookies

The user has an iframe with storage access and loads a same-origin iframe nested inside the first iframe. The subdocument fetch behaves differently among browsers. In the spec, I believe it is ambiguous whether or not this request should get unpartitioned cookies, pending the result of the cookie-layering work. However it is clear that the resulting window should not have storage access initially.

Chrome now sends unpartitioned cookies on the subdocument fetch, then does not give access to the subdocument's unpartitioned cookies initially. This is weird, and I think we should make these two align. Firefox gives neither unpartitioned cookies. This is consistent, but increases developer friction.

To solve this, I propose we generalize our propogation of the has storage access state to not be self-initiated but instead be same-origin-initiated. This does not meaningfully change the security properties of which documents may get storage access in my view, maintaining origin-granularity over which subdocuments can ever access their unpartitioned cookies.

@bvandersloot-mozilla
Copy link
Collaborator Author

@arturjanc: you had security concerns on the origin boundary w.r.t. navigations initiated by documents with storage access. Does this maintain the security invariants you wanted? I don't recall why the navigation was required to be self-initiated.

@arturjanc
Copy link

This change makes sense to me and I don't think it will undermine the security properties we care about here.

The motivation behind the self-initiated restriction was to prevent a cross-origin embedder from navigating an iframe that received storage access to an arbitrary endpoint on the iframe's site chosen by the embedder (and e.g. clickjack it or leak data). But allowing same-origin-initiated navigations to maintain storage access is fine because it still protects the iframe from these kinds of navigations, so I think we can safely allow it.

@annevk
Copy link
Collaborator

annevk commented Mar 7, 2024

I think @johannhof was also interested in allowing this for the same site, no?

@arturjanc
Copy link

Dumb question: can you even initiate a navigation in a same-site-but-cross-origin iframe if you're not the embedder? You generally shouldn't be able to navigate cross-origin frames except in a few cases, e.g. you have an embedder/embeddee relationship - I think we tightened this a few years ago, but don't remember exactly where we landed on that.

Basically, I'm not sure in which scenarios allowing same-site navigation would be useful here.

@annevk
Copy link
Collaborator

annevk commented Mar 7, 2024

That's not something that's currently well-defined unfortunately. whatwg/html#313 goes into some of it.

@jsnajdr
Copy link

jsnajdr commented Mar 8, 2024

Hi 👋 I'm the reporter of the Firefox bug that @bvandersloot-mozilla mentions. And the question whether storage access should be propagated to a nested same-site or same-origin iframe is alse very relevant to us.

Our setup looks like this:

  1. Third party sites embed an iframe from widgets.wordpress.com. This iframe requests storage access, and after it's granted, it expects to be able to issue credentialed fetch requests and load credentialed sub-iframes, with the unpartitioned login cookie the user got when logging into wordpress.com as a top-level site.
  2. The widgets.wordpress.com iframe loads a nested iframe, from public-api.wordpress.com. (Cross-origin, same-site). This nested iframe would like to be loaded with wordpress.com unpartitioned login cookie. This currently works on Chrome, but doesn't work on Firefox.
  3. The public-api.wordpress.com nested iframe would also like to send credentialed fetch requests to public-api.wordpress.com (same-origin). This currently doesn't work, neither in Chrome nor Firefox, because the storage access is not automatically propagated to the nested iframe. And it can't request storage access on its own, because there are no user interactions with this nested iframe. It's an invisible helper that communicates with its parent using messages.

Reading the discussion in this issue, it seems that propagating the storage access automatically to nested same-site iframes could be possible to standardize and implement? That would solve all our problems.

After all, if widgets.wordpress.com was a top-level site and it embedded public-api.wordpress.com, the iframe would get storage access, because:

If browsingContext is same authority with browsingContext’s top-level browsing context's active document, resolve p with true.

But when this tree of same-site iframes is embedded in a cross-origin document, and the top-level iframe of this tree is granted storage access, it's a similar situation, but the nested iframes don't get access.

@cfredric
Copy link
Contributor

cfredric commented Mar 8, 2024

My concern with a change like this is that it makes it hard to "drop" unpartitioned cookie access once you've gotten it, even if you want to. Today, an iframe can drop cookie access just by opening a new iframe and not requesting access there. With this change, that's no longer possible. It seems like a step backward to the unsafe default of having unpartitioned cookie access without having asked for it, IMO.

Instead, I think a better solution for this is to use the Storage Access Headers proposal, specifically the load response header. Then the subdocument fetch will still be credentialed (since the parent iframe has unpartitioned cookie access, after all), and the fetch's response headers will indicate that the subresource iframe is opting into having unpartitioned cookie access, upon loading. This ensures that any iframe that wants cookie access still has to ask for it explicitly, which is a better default policy for security IMO.

@jsnajdr, I think there might be a misunderstanding on your third point - although storage access is not automatically propagated to the nested iframe, the nested iframe can call requestStorageAccess() without obtaining a user gesture first (at least in Chrome and Firefox), and the request will be granted since the user has already granted permission to the parent iframe. Can you give that a try?

@bvandersloot-mozilla
Copy link
Collaborator Author

I like that approach, if we can get a 3-engine consensus on the storage access headers :)

@jsnajdr
Copy link

jsnajdr commented Mar 11, 2024

although storage access is not automatically propagated to the nested iframe, the nested iframe can call requestStorageAccess() [...] Can you give that a try?

Oh yes, this has been the missing piece and it works! And it works around the Firefox "bug" I reported. Initially, the frame load request is sent without unpartitioned cookies and the frame doesn't have storage access. But I can do this:

if (!await document.hasStorageAccess()) {
  await document.requestStorageAccess();
  window.location.reload();
}

After the access was granted, I reload the frame. This time the request is sent with unpartitioned cookies, and after the load the frame has storage access automatically, without having to ask for it.

With Storage Access Headers, the above script can be replaced with a response header Activate-Storage-Access: retry. Then the browser will do the same workflow automatically. However, I believe I still need to load the iframe twice, I don't see a way how to save the extra roundtrip.

It seems like a step backward to the unsafe default of having unpartitioned cookie access without having asked for it

I see, the design principle is that no embedded frame should automatically get storage access until it asks for it explicitly. The default is "no access".

But that means that Firefox does it right (not sending unpartitioned cookies on the initial load) and it's Chrome that has a bug. If the embedded frame doesn't have storage access by default, then the load request shouldn't have unpartitioned cookies. If it has them, it means that the frame effectively does have unpartitioned access, because the server response can contain data derived from the unpartitioned cookies.

@cfredric
Copy link
Contributor

However, I believe I still need to load the iframe twice, I don't see a way how to save the extra roundtrip.

Yeah, there's a tradeoff between security and performance here. We're exploring that tradeoff space in privacycg/storage-access-headers#6; feel free to take a look and contribute if you have more ideas.

But that means that Firefox does it right (not sending unpartitioned cookies on the initial load) and it's Chrome that has a bug. If the embedded frame doesn't have storage access by default, then the load request shouldn't have unpartitioned cookies.

Maybe I'm misunderstanding, but from your initial description I understood that the embedded iframe does request storage access: This iframe requests storage access, and after it's granted, it expects to be able to issue credentialed fetch requests. One of the credentialed fetches that this iframe issues happens to be for the source of another iframe (which should not implicitly have storage access when it loads, IMO), but that doesn't change that the original iframe has storage access and can issue credentialed fetches. From that point of view, I think Chrome is behaving consistently here.

@jsnajdr
Copy link

jsnajdr commented Mar 12, 2024

One of the credentialed fetches that this iframe issues happens to be for the source of another iframe (which should not implicitly have storage access when it loads, IMO)

This is the core question that Firefox and Chrome are answering differently. Which iframe really does the fetch? Is it the parent iframe (and its document and window objects) or is it the child iframe?

Chrome thinks that the parent iframe does the fetch, doing it for the child iframe. Firefox thinks that the child frame does it, doing it for itself.

Is there a place in the HTML or Fetch standard that would state clearly which document owns the fetch? I would say it should be the child frame's contentDocument: that frame has its own "content navigable" and the fetch is part of its session history.

I noticed another difference between how Firefox and Chrome treat the iframe fetch differently. When there is a widgets.wordpress.com embedded frame that creates a nested public-api.wordpress.com iframe, the fetch has these headers in both browsers:

Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-site

Here, both browsers agree that the fetch is not same-origin, but same-site. It's like the parent frame owns the fetch. But when the frame reloads itself (in the "if doesn't have storage access, request access and reload" flow), then Chrome treats the second fetch as same-origin:

Sec-Fetch-Site: same-origin

But in Firefox it's still same-site, just like the initial request.

@johannhof
Copy link
Member

As to "who does the fetch", I would defer to @annevk for authoritative knowledge of the current specification. In any case, I don't think we should be dogmatic about what the current spec says but consider what the best developer experience would be instead.

I agree that it's very confusing to have a credentialed document load and then no subsequent storage access in the document, since developers are likely to already make a decision about what content to render based on the server-side presence of cookies or not. It could also lead to subtle issues where most of the pre-rendered page works well but then subsequent credentialed subresource requests fail.

So, either we completely exclude nested same-site iframes from receiving storage access without rSA (or headers) or we always propagate the storage access bit to same-site children.

I'm leaning towards the latter. I haven't really seen clear evidence of a practical attack that would be possible against those nested iframes, especially one that isn't also applicable to any other same-site resource loaded by the original iframe. Without this kind of reasoning for restricting it, I think we should prefer developer utility and simplicity here.

@jsnajdr
Copy link

jsnajdr commented Mar 18, 2024

So, either we completely exclude nested same-site iframes from receiving storage access without rSA (or headers) or we always propagate the storage access bit to same-site children.

I was testing this with Safari (17.2.1) today and found that it already propagates the access to a nested same-site iframe:

  1. Top-level site example.com embeds iframe from widgets.wordpress.com
  2. widgets.wordpress.com requests access and it's granted. (On a second load it doesn't even need to request the access, it's auto-granted, which is nice). Then this iframe loads nested iframe from public-api.wordpress.com.
  3. public-api.wordpress.com has access (await document.hasStorageAccess() === true) right from the beginning, and doesn't need to request it.

However, there is a weird bug when wordpress.com is the top-level site. Then a nested public-api.wordpress.com iframe doesn't have storage access! It needs to ask for it with requestStorageAccess(), and additionally, that request needs to be triggered by a user interaction. Otherwise it rejects with undefined. I.e., what @cfredric recommends above:

although storage access is not automatically propagated to the nested iframe, the nested iframe can call requestStorageAccess() without obtaining a user gesture first (at least in Chrome and Firefox), and the request will be granted

doesn't work in Safari when a top-level frame (which never asked for access, because why would it when it's the top-level document) embeds a same-site iframe.

This is already reported in WebKit since 4 years ago, and @johnwilander responded "I made a review comment about this in the ongoing Storage Access API spec work." But there are no details about the location and the content of the review comment, and the bug is still there today.

@arturjanc
Copy link

So, either we completely exclude nested same-site iframes from receiving storage access without rSA (or headers) or we always propagate the storage access bit to same-site children.

I'm leaning towards the latter. I haven't really seen clear evidence of a practical attack that would be possible against those nested iframes, especially one that isn't also applicable to any other same-site resource loaded by the original iframe. Without this kind of reasoning for restricting it, I think we should prefer developer utility and simplicity here.

Conceptually, I see this similarly to what @cfredric outlined above, i.e. it seems a bit cleaner for each document to require explicit calls to document.requestStorageAccess() to receive credentials when embedded in top-level-3P context. This way, having one document within a site that calls rSA doesn't suddenly make any content from its hosting site eligible to be embedded with credentials (if e.g. the document that received storage access directly iframes it).

OTOH, looking at this purely from a security perspective I think @johannhof is right that we don't have a compelling reason to disallow automatic propagation of storage access:

  • We preserve storage access on self-initiated navigations of the iframe, so it's already possible that a document that doesn't have rSA will have storage access in a 3P context.
  • Headers that restrict iframing (X-Frame-Options and frame-ancestors in CSP) check every document in the ancestor chain. So if there's a sensitive endpoint that doesn't want to be iframed cross-site, then even if it's unexpectedly embedded as a nested frame with storage access in a 3P context, it will still not render if it sets one of these headers.

One concern that @ddworken came up with is whether a cross-site ancestor, e.g. the top-level page, is permitted to navigate the nested frame (which, as @annevk points out above, is not well-defined) -- my hope is that it shouldn't. But if it was, then it could navigate that nested frame to a different endpoint same-site with the frame that requested storage access; so we would need to prevent this from happening, similarly to the self-initiated restriction we discussed above.

Overall, to me this boils down to the ergonomic benefit of automatically propagating storage access. Ideally, if this affects only a small number of widgets, we could forego this and require nested frames to call rSA. But if this introduces a burden on many developers building widget that request storage access, I think automatically propagating access to same-site frames would be okay.

@jsnajdr
Copy link

jsnajdr commented Jul 24, 2024

Following up on the discussion about whether an iframe load request should initially have storage access or not. There is a surprising relation to CHIPS and partitioned cookies, as pointed out in privacycg/CHIPS#88.

Depending on whether the request has storage access or not, a different set of cookies will be sent with it, and the server will send different Set-Cookie headers. What if the Set-Cookie cookies have the partitioned flag? Then both has-storage and no-storage iframes can share the same partition, and they will overwrite each other's cookies or info will leak from a has-storage to a no-storage iframe.

That means that the hasStorageAccess bit should be part of the partition key. Currently browsers implement a "has foreign ancestor" bit, but having a foreign ancestor is merely one of several ways how the same iframe embedded in a different way can sometimes have and sometimes not have storage access.

@cfredric
Copy link
Contributor

@jsnajdr the question of how CHIPS partition keys should be defined seems like it's best discussed in privacycg/CHIPS#88; let's discuss there, since that question seems somewhat unrelated to this issue.

(With that said, I agree with what @aselya said there. IMO it would be very confusing if an iframe's partition key could change over time; and the isolation that you're trying to achieve is still possible by simply not using partitioned storage.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants