-
Notifications
You must be signed in to change notification settings - Fork 31
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
Embedded iframes with different storage access share (and overwrite) the same partitioned cookie #88
Comments
I found that Firefox has a function called
That's how Firefox manages to keep both iframes partitioned storage separate. Chrome doesn't have this foreign ancestor flag and the partition key for both iframes is exactly the same, leading to bad behavior. FYI @bvandersloot-mozilla who might know more context why the foreign flag is there. It's clearly an are where the same platform feature behaves very differently in different browsers. |
@jsnajdr, thank you for the detailed explanation of the issue. I believe that this is an issue that might be addressed by my work including the ancestor chain bit in the partition key of partition cookies. I will need to do some more investigation but while I do that would you mind trying your test site either using the most recent build of Chrome Canary and let me know if the current implementation addresses the issue. |
Thank you @aselya for the quick response. I tried the latest Chrome Canary and I can confirm that the new foreign ancestor bit works as expected. The two embedded iframes in my test site (A->A and A->B->A) no longer share the same partition and don't overwrite each other's cookies. Chrome now behaves exactly the same as Firefox (Nightly). However, it's still possible to create a situation where logged-in and logged-out iframes share the same partition and overwrite each other's cookies. Here's a test site running at The iframes don't have storage access to Now let's click "Request access & reload" in the left iframe. It will do That suggests that keying the partition with the foreign ancestor bit might be a design mistake. The correct bit to key with is the |
Thank you for taking the time to do the additional testing. I’m glad to hear that the ancestor bit is behaving as expected and that the behavior aligns with Firefox (Nightly). The addition of the ancestor chain bit to the partition key of partitioned cookies adds a limited level of security by differentiating between cookies set in a cross-site and same-site context but the primary concern for partitioned cookies is mitigating cross-site tracking. As you observed, the partition keys do not differentiate between individual frames. This behavior is consistent with how the rest of storage is partitioned in the browser as there are no security or privacy barriers between iframes with the same frame ancestors. Your use case may be better addressed through the use of unpartitioned cookies and using StorageAccessApi (SAA). Since the access granted by SAA is per-frame unlike partitioned cookies. Another approach to consider if you require partitioned cookies, is to utilize a fenced-frame instead of an iframe. It places more restrictions on the cross-site data in embedded content than an iframe and will result in a CookiePartitionKey that does not match between iframes. However, fenced frames are not universally supported by all major browsers and fenced frames cannot use requestStorageAccess to gain access to unpartitioned storage. |
Our use case involves:
In short, we want to use SAA together with partitioned cookies and I claim that they don't work together well.
Yes, and there also no barriers between same-origin iframes that are embedded in a cross-origin nested structure. Any iframe can traverse And this is a paradoxical thing about Storage Access API: although all same-origin frames are on par security-wise, some of them will have storage access and some won't. A frame that doesn't have storage access can easily circumvent that by finding a same-origin iframe that has storage access and communicating with it.
I'd say it's much less confusing than it seems at first. Because when an iframe discovers that it has been loaded without storage access, a typical action is to request access and reload. The storage access headers proposal mentions this technique all the time. When the change of partition key is associated with a reload, it feels much more intuitive. The storage access headers proposal also mentions a calendar widget example that loads a "placeholder" without storage access, and this placeholder requests access and reloads. I see two problems with this example:
|
Let me see if I understand your use case/requirements. From what I gather:
From that perspective, it feels to me that your problem isn't solvable with cookies, since cookies really aren't "per-window" state. Even partitioned cookies (or DOM storage) cannot help, because partitions are meant to be shared by different iframes (as long as there are no privacy concerns with doing so) - which seems like it violates your second requirement. I don't quite follow the reasoning behind your second requirement, but IMO, it seems similar to some concerns around CSRF, and I wonder if using an iframe-local token (like an anti-CSRF token) would satisfy your requirements. That kind of solution uses per-iframe state, rather than per-partition state, so I think it has the data-isolation property you are looking for. |
No, the requirement is much simpler than this. The authentication state is stored in a first-party cookie. So any embedded iframe can be only in two possible states: 1) it has storage access to the first-party cookie, and therefore it's authenticated. 2) it doesn't have storage access, and therefore it's logged out. Now imagine the embedded frame wants to set a partitioned cookie. And the cookie's value is derived from authentication state. This is a perfectly reasonable scenario, there's nothing weird about it. The CHIPS standard describes two motivational examples: store finder service sets your preferred location as a partitioned cookie, or a support chat service sets your conversation ID as a partitioned cookie. If the service knows who you are (via its first party auth cookie), it's reasonable that it sets a different location or conversation ID compared to when it doesn't know who you are. But when you embed two iframes from the same service, and one has storage access and the other doesn't, the partitioned cookies will have different values, and will overwrite each other, because they share the same partition. Without SAA, this cannot happen. If two frames have different "storage shelves" to read from, they also have different storage shelves to write to:
Now SAA adds another situation where the storage shelf for reading is different for two iframes, but the writes are not sufficiently isolated yet. That's the bug I'm reporting.
Our use case uses partitioned cookies to "forward" a cookie to another domain:
Yes, this is very similar to setting a CSRF token. And yes, in the end we'll be able to solve this without cookies, because we don't need the token to persist between navigations: each iframe load is able to generate a new one and store it in the response body. But if we wanted the token to persist, like in the "store finder" and "chat widget" scenarios, then we'd have to store it in storage, be it cookies or local storage, the current partitoning behavior with SAA would be an obstacle. |
Thanks for the clarifications!
This makes sense to me; this is a good fit for an unpartitioned cookie (or several).
Since the two iframes know a priori that they have different authentication states (and will therefore write different values into the partitioned storage medium), couldn't they use different names for the cookie(s) too, to avoid overwriting each other? That's a simple change that avoids coupling unpartitioned and partitioned storage via the key, and it sounds like it would solve your issue. I would prefer to avoid incorporating the "has storage access" bit into the partition key, because:
What do you think? Am I missing something about your requirements that makes using two different cookies a non-viable approach? |
Yes, at least the server knows whether the iframe is loading with storage access or without. Because the load request includes first party cookies. So, if a store finder wants to set a
Now, in the browser, both iframes will see both cookies (if they are not let location = getDocCookie('preferred_location_fpno');
if (!location && await document.hasStorageAccess()) {
location = getDocCookie('preferred_location_fpyes');
} (even with storage access we have to look at the For So, it can all work, and it's very similar to the advice in this document about transitioning to partitioned cookies.
However, I'm not very sure about this. The iframe that doesn't have storage access will still see the It's the same situation as with the foreign ancestor bit discussed in privacycg/storage-partitioning#25. Information obtained using a So, if leaking samesite-derived information is a privacy and security concern, then leaking first-party-derived information causes exactly the same concerns, whatever they are, doesn't it? One difference might be that privacycg/storage-partitioning#25 is very much about service workers. Does SAA and CHIPS have any relevant interaction with service workers? That's a point where I start to be very confused 🙂 |
Agreed; the script ultimately wants to know if it is logged in (i.e. if it has access to the unpartitioned auth cookies, and if the auth cookie exists), so
Recall that our assumption is that one of the iframes already obtained From a security perspective, we need to consider the threat model. The reason that the ancestor chain bit was added is that the inner But that isn't the case in the situation you've described; both of the iframes are cross-site, so they are equally vulnerable to attack by a malicious embedder.
From the browser's point of view, this isn't a leak: your site willingly copied the same-site-derived information from unpartitioned cookies and put it in partitioned storage. If you decide that you consider that a leak, then you should store the data in the unpartitioned storage/cookies, and stop copying it into cross-site partitioned storage. Then that info would be available if and only if the iframe has storage access, which sounds like what you actually want.
I believe service workers use the same partition key that CHIPS uses (including the ancestor chain bit). Service workers don't have any interaction with SAA, due to security concerns there. |
At wordpress.com we tried to enable the Partitioned attribute on some of our auth cookies and then had to quickly revert the changes because we discovered the following problem.
At
jardasn.dev
I prepared a minimal reproduction example that shows what's going wrong. After visitingjardasn.dev
and clicking "Login" you'll see a page with two embedded iframe. This is how it looks like in Chrome (with blocked 3rd party cookies):The site has a top-level login cookie called
logged_in
. And there are two iframes that embed a document from a same-site same-origin URL. One is embedded directly, the other is nested in an intermediate cross-origin iframe fromjardasn-alt.dev
.When loading the
jardasn.dev/embed
document, the response includes aSet-Cookie
header with another cookie,logged_in_rest
. When the request has thelogged_in
cookie, thenlogged_in_rest
value will be the same aslogged_in
, i.e.,admin
. When the request doesn't havelogged_in
cookie, the value will beanon
. Both cookies are supposed to be in sync. Thelogged_in_rest
cookie is partitioned, it's supposed to be "private" to the embed iframe. Think of it as an extra authentication token, or nonce, specific for the embed.The second iframe is nested inside cross-origin
jardasn-alt.dev
iframe. That makes Chrome treat it as third party. Therefore thelogged_in
cookie will be blocked when thejardasn.dev/embed
is loaded, and the iframe will not have storage access. Thelogged_in_rest
cookie returned by the server is going to beanon
.The first iframe is logged in, the second is logged out. However, both iframes share the same partitioned storage! The second iframe loaded a bit later, and when storing the
logged_in_rest=anon
cookie, it set it also for the first iframe! If you click the "Reread document.cookie" button in the first iframe, you'll see that the value changes fromadmin
toanon
:That's because the second iframe stored a new value. This is a big problem because instead of having one frame fully logged in and another fully logged out, the iframes pollute each other's cookies and the first frame becomes half logged out.
Let's send a REST request from both iframes. Clicking the "Send REST request" will trigger a same-origin
fetch()
and in the response the server will tell us which cookies did the request have:The first iframe's request cookies are out of sync.
logged_in
(first party cookie) isadmin
, but thelogged_in_rest
isanon
, like if the iframe was logged out.The second iframe's request cookie are OK. The
logged_in
one is missing because it's a first party cookie and it's blocked because there's no storage access. And the partitionedlogged_in_rest
cookie also says the request is anonymous.If the first iframe loaded second, it would lead to a similar situation where the second frame is broken. It wouldn't have a valid
logged_in
cookie available, but it would have a validlogged_in_rest=admin
. The login information would leak into the logged out iframe!The root cause of all this is that both iframes share the same partition key, while having different first-party cookie access.
Firefox behaves differently. (I use the Nightly version with
optInPartitioning
enabled) Apparently both cookies are stored with different partition keys because they have two independent values:The first frame is fully logged in, the second is fully logged out, each has its own
logged_in_rest
cookie and clicking "Re-read document.cookie" doesn't change the value. Both REST requests have two cookies that are in sync.Safari also behaves differently. It doesn't support partitioned cookies, but the key difference is that both iframes do have storage access, so they both see the same first party
logged_in
andlogged_in_rest
cookies:It seems that Safari doesn't care about the intermediate cross-origin
jardasn-alt.dev
iframe and that it gives the nested iframe full storage access anyway.It's only Chrome where the problematic situation happens: shared partitioned storage, different storage access.
The second iframe can "fix itself" by requesting storage access and immediately reloading:
The iframe is already "entitled" to have storage access, it just has to ask for it. And the request is exempt from the usual limitations -- doesn't need to be triggered by user gesture, doesn't show a popup.
But anyway, while the iframe is reloading, the bad
logged_in_rest
cookie is still in the shared storage and can cause damage. Only after it reloads it sets the right cookie and both iframes are logged in.The text was updated successfully, but these errors were encountered: