-
Notifications
You must be signed in to change notification settings - Fork 38
Description
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 visiting jardasn.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 from jardasn-alt.dev
.
When loading the jardasn.dev/embed
document, the response includes a Set-Cookie
header with another cookie, logged_in_rest
. When the request has the logged_in
cookie, then logged_in_rest
value will be the same as logged_in
, i.e., admin
. When the request doesn't have logged_in
cookie, the value will be anon
. Both cookies are supposed to be in sync. The logged_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 the logged_in
cookie will be blocked when the jardasn.dev/embed
is loaded, and the iframe will not have storage access. The logged_in_rest
cookie returned by the server is going to be anon
.
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 from admin
to anon
:
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) is admin
, but the logged_in_rest
is anon
, 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 partitioned logged_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 valid logged_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
and logged_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:
if (!await document.hasStorageAccess()) {
await document.requestStorageAccess();
document.location.reload();
}
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.