privacycg / CHIPS

A proposal for a cookie attribute to partition cross-site cookies by top-level site

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How do `Partitioned` cookies interact with browser extensions?

DCtheTall opened this issue · comments

Chrome and Firefox both give developers the ability to build extensions which have background scripts which can interact with a site's cookie jar if the extension has host permissions for that site (documentation of these APIs for Firefox, Chrome).

It is not clear to us how we can surface Partitioned cookies to these APIs.

For example, an extension could have host permission for a site that is open in a top-level context in one browser tab and an embedded cross-site context in another tab. In this case, which partition should we surface to the background script cookies API?

One option is to only show the site's first-party cookies in this API, but this can potentially expose a cross-site tracking side channel. Consider an extension owned by tracker.com which has host permissions for that site. Every time tracker.com is loaded in an embedded context and detects the extension on the user's machine, it could use a content script that queries the background script for its first-party cookies which it can use to join cross-site cookies across partitions. It seems this attack would still be possible if the Chrome extension's background script gets its own partition as well.

One option is to block partitioned cookies to background scripts, but this is a severe route to take and also may be problematic if browsers end up partitioning their cookie jars by default.

Another option is to only show partitioned cookies to the extension for the top-level contexts the browser is opening. However there are several drawbacks to this approach: it seems difficult to implement correctly and it causes the background script cookies API to behave inconsistently based on the state of the browser, making it harder for developers to test.

@johannhof since you have worked on state partitioning in Firefox, my understanding is that Firefox extensions have the background script specify which top-level partition it wants cookies for using the firstPartyDomain, is that correct? ref

Does the extension need host permissions for both the cookies' site and the firstPartyDomain, or just the former?

Hi @DCtheTall, this is generally the approach that we're taking, yes. However note that what that documentation describes is the First-Party-Isolation (FPI) compatible API, which is different from our recent work on State Partitioning (internally also called dFPI).

To give some more context, FPI is in use in e.g. Tor browser to isolate third parties, but for State Partitioning we had a few incompatible requirements that required us to use a different origin attribute, called partitionKey (vs. firstPartyDomain for FPI).

The WebExtension API support for partitionKey is tracked in this bug and it's not done yet. There's some discussion around how the semantics of that should work, i.e. whether it should be a separate attribute or the same, depending on the browser setting. But generally I think the idea of "let the extension explicitly decide what it wants" will hold.

cc @Rob--W who is currently assigned to that bug.

There's also an open bug for supporting the downloads API.

Does the extension need host permissions for both the cookies' site and the firstPartyDomain, or just the former?

I would assume it's just the former, but I'll verify that again.

@johannhof thanks for the response. I was wondering when you were working on your solution for making extension's cookies API compatible with partitioned cookies, did you all consider if extensions might try to join cookies across partitions? Is this something you think browser vendors need to be concerned about?

Thanks!

@johannhof thanks for the response. I was wondering when you were working on your solution for making extension's cookies API compatible with partitioned cookies, did you all consider if extensions might try to join cookies across partitions? Is this something you think browser vendors need to be concerned about?

I'll answer since I'm working on this feature and was also involved in similar before (cookies API support for FPI). There are two ways to interpret your question:

  1. Will the API enable extensions to inadvertently mix cookies from different partitions?
  2. The API enables extensions to deliberately mix cookies from different partitions. Is this a concern?

1 was solved for FPI by requiring the firstPartyDomain attribute to be set when FPI is enabled when creating/updating cookies. We could require something similar when dFPI is supported (through firstPartyDomain or a new property). That prevents extensions from working when dFPI is enabled until they add support for (d)FPI.

2 is intentional. Extensions with permissions to cookies and host permissions for a specific host can read/modify cookies for the given domain. Once a user has granted the extension access to do so, we permit extensions to freely use these permissions. This includes support for deliberately mixing cookies from partitions if an extension chooses to do so. A legitimate use case with this characteristic is to copy cookies from one partition to another to duplicate a session.

Firefox 94 supports the partitionKey option as a way to interact with cookies in the extension API (https://bugzilla.mozilla.org/show_bug.cgi?id=1669716).

I experimented with a string at first, but ended up shipping with an object value, because it enables a more intuitive extension API and it's also possible to support new values in the future. A notable characteristic is that partitioned cookies are not exposed through cookies.getAll by default, because extensions that are unaware of partitioned cookies would too easily copy partitioned cookies to the non-partitioned storage (the way to update a cookie is to get the cookie and then call cookies.set with the same info, omission of the partitionKey key would result in the creation of a cookie in unpartitioned storage).

Here is a summary of the API design, examples and semantics: https://phabricator.services.mozilla.com/D123491#4106552

Please update the spec to match this format (basically partitionKey: "http://site to partitionKey: { topLevelSite: "http://site" }). The object format enables future extension, e.g. partitionKey: { firstPartySite: "http://site" } (IF some different syntax were to be needed for First-Party-Sets).

For the future, I'm considering the addition of a new API, cookies.getPartitionKey to generate the partition key for a given frame or URL. For now I shipped without such a convenient helper, because the current format of the API allows extensions to already do the right thing for existing cookies (and the logic can be figured out by observing the logic, or using document.cookie to set a temporary cookie and then observing the result).

Hey @Rob--W, thanks for the update!

Using a plain object for extensibility makes sense to me in case we want semantics other than top-level site to determine a cookie's partition key.

One question I have is do you intend to return only partition keys when the partitionKey field is present, or return the partitioned cookies in addition to the unpartitioned cookies that match the other query filter parameters?

Another question I have is regarding partitioning by First-Party Set. If a 3P, embed.com, sets a cookie on member.com which is in a member of a First-Party Set owned by owner.com, then do you intend to have extension API users query for that cookie using partitionKey: {topLevelSite: 'https://member.com'} and/or partitionKey: {firstPartySite: 'https://owner.com'}? I'd assume we'd only return cookies in the set's partition in the latter case. My reasoning is that cookies in the set's partition would be sent to embeds all top-level sites in the set, so it isn't meaningful to assign a top-level site to cookies in the set's partition.

Another case to consider is when users enable/disable First-Party Sets. If a user has FPS disabled and embed.com sets a cookie on member.com, then I think that cookie should remain in a separate partition from the set's (keyed on member.com) since that cookie was set with the expectation of top-level site partitioning. In that case, would those cookies only be available to extensions that query partitionKey: {topLevelSite: 'https://member.com'} and not partitionKey: {firstPartySite: 'https://owner.com'}?

One question I have is do you intend to return only partition keys when the partitionKey field is present, or return the partitioned cookies in addition to the unpartitioned cookies that match the other query filter parameters?

Non-partitioned cookies are represented by partitionKey: null, which is currently equivalent to the following:

  • partitionKey: null
  • partitionKey: {}
  • partitionKey: { topLevelSite: null }
  • partitionKey: { topLevelSite: "" }.
  • partitionKey: {}

Partitioned cookies use partitionKey object with a non-empty topLevelSite (or possibly another key-value pair if partitioned by other properties, in the future).

This format is used in cookies.set, cookies.get and cookies.remove. These APIs operate on individual / single cookies.

The cookies.getAll API offers a way to query multiple cookies. partitionKey: null would still return unpartitioned cookies (which effectively hides partitioned cookies from extensions that aren't prepared to deal with it). When partitionKey is set to any value, partitioned cookies may be returned. Examples:

  • cookies.getAll({ partitionKey: {} }) returns all partitioned and non-partitioned cookies.
  • cookies.getAll({ partitionKey: { topLevelSite: null } }) returns all partitioned and non-partitioned cookies.
  • cookies.getAll({ partitionKey: { topLevelSite: "" } }) returns non-partitioned cookies.
  • cookies.getAll({ partitionKey: { topLevelSite: "https://site" } }) returns partitioned cookies.

The idea behind this syntax is that the value of partitionKey in cookies.getAll is used to filter cookies. If a property is null/void or simply not set, then cookies.getAll does not filter by that property. If a specific property is set, then that filter is enforced.

If one wants to retrieve "all cookies at https://site" as in "third-party cookies at https://site (partitioned) + first-party cookies at https://site (unpartitioned)", then they need to perform two calls to cookies.getAll. The set of cookies are disjoint, so it is not strictly necessary to support the ability to query both sets of cookies at once via the cookies.getAll API (if desired, one can introduce a new option in the cookies.getAll option, possibly a member of partitionKey).

Another question I have is regarding partitioning by First-Party Set. If a 3P, embed.com, sets a cookie on member.com which is in a member of a First-Party Set owned by owner.com, then do you intend to have extension API users query for that cookie using partitionKey: {topLevelSite: 'https://member.com'} and/or partitionKey: {firstPartySite: 'https://owner.com'}? I'd assume we'd only return cookies in the set's partition in the latter case. My reasoning is that cookies in the set's partition would be sent to embeds all top-level sites in the set, so it isn't meaningful to assign a top-level site to cookies in the set's partition.

Note that First-Party Sets are not implemented in Firefox, and I have no specific implementation experience with it. I would share my opinion of what I would think though.

In your example, there is a 3P in embed.com at 1P member.com (which is part of FPS owner.com). Depending on the presence of the SameParty flag, the partition would be as follows:

  • Set-Cookie: name=value; Secure; SameSite=Lax would have partitionKey: {topLevelSite: "https://member.com"}
  • Set-Cookie: name=value; Secure; SameSite=Lax; SameParty would have partitionKey: {firstPartySite: "https://owner.com" }. topLevelSite could be included if you'd like, but it would seemingly not be very meaningful since the firstPartySite field already communicates the same information. If the site is moved out of a FPS, then the information would be lost.

An issue with FPS and generally site-based cookie partitioning is that the "primary key" is not necessarily fixed. A "site" is the eTLD+1 (effective TLD + name, where the effective TLD is derived from the Public Suffix List). In Firefox's implementation of partitionKey in the cookies API, the topLevelSite's value is canonicalized to the eTLD+1. E.g. https://www.example.com/ignoreme becomes https://example.com. I would expect the implementation using First-Party Sets to do something similar: the cookies API should choose the canonical representation based on the input.

If the generation of partitionKey becomes considerably more complicated in the future, then I'd imagine that we can introduce a new cookies.getPartitionKey method that returns a partitionKey for a given tab/frame, URL and/or first-partiness and/or First-Party.

Another case to consider is when users enable/disable First-Party Sets. If a user has FPS disabled and embed.com sets a cookie on member.com, then I think that cookie should remain in a separate partition from the set's (keyed on member.com) since that cookie was set with the expectation of top-level site partitioning. In that case, would those cookies only be available to extensions that query partitionKey: {topLevelSite: 'https://member.com'} and not partitionKey: {firstPartySite: 'https://owner.com'}?

The choice of partition of (existing) cookies upon toggling FPS is not a concern of the extension API. That's up to the implementation. IMO extensions need to be able to discern non-partitioned cookies, topLevelSite-partitioned cookies and firstPartySet-partitioned cookies. The choice of partitionKey does not change for pre-existing cookies when FPS is toggled (but the syntax to query new cookies may change, since the partition differs). If FPS is toggled, then those cookies won't be sent to sites in practice, but they can still be read and modified via the extension API. This enables the development of extensions that serve as tools to migrate cookies between partitions.

One note to consider: Previously I mentioned that topLevelSite is automatically canonicalized. If an internal cookie is no longer stored in a canonical form, then the automatic canonicalization can prevent extension APIs from modifying that unreachable cookie. A way around this is to introduce a new property in the partitionKey object, e.g. a flag to signal that the input should not be canonicalized. This flag would be set on the cookies returned from the extension API. Extensions don't need to pay special attention to the exact set of properties - they simply need to read partitionKey from the returned cookies and pass partitionKey to the cookies.set or cookies.remove API.

Thanks for your detailed reply and explanations.

An issue with FPS and generally site-based cookie partitioning is that the "primary key" is not necessarily fixed. A "site" is the eTLD+1 (effective TLD + name, where the effective TLD is derived from the Public Suffix List). In Firefox's implementation of partitionKey in the cookies API, the topLevelSite's value is canonicalized to the eTLD+1. E.g. https://www.example.com/ignoreme becomes https://example.com. I would expect the implementation using First-Party Sets to do something similar: the cookies API should choose the canonical representation based on the input.

First-Party sets have an "owner" site, so we would make the partition key https://owner.com (FPS are required to use the HTTPS scheme). The idea being that partitioning by First-Party Set mirrors top-level site partitioning, but behaves as if the top-level site was owner.com if the browser is visiting any site in owner.com's set (previously existing mechanisms like site for cookies won't do this, just cookie partitioning).

If the generation of partitionKey becomes considerably more complicated in the future, then I'd imagine that we can introduce a new cookies.getPartitionKey method that returns a partitionKey for a given tab/frame, URL and/or first-partiness and/or First-Party.

I agree this API would probably be useful for extensions in the future.

One note to consider: Previously I mentioned that topLevelSite is automatically canonicalized. If an internal cookie is no longer stored in a canonical form, then the automatic canonicalization can prevent extension APIs from modifying that unreachable cookie. A way around this is to introduce a new property in the partitionKey object, e.g. a flag to signal that the input should not be canonicalized. This flag would be set on the cookies returned from the extension API.

Interesting. Is the idea that you would check each cookies' internal representation of their partition key when they're loaded from disk?

An issue with FPS and generally site-based cookie partitioning is that the "primary key" is not necessarily fixed. A "site" is the eTLD+1 (effective TLD + name, where the effective TLD is derived from the Public Suffix List). In Firefox's implementation of partitionKey in the cookies API, the topLevelSite's value is canonicalized to the eTLD+1. E.g. https://www.example.com/ignoreme becomes https://example.com. I would expect the implementation using First-Party Sets to do something similar: the cookies API should choose the canonical representation based on the input.

First-Party sets have an "owner" site, so we would make the partition key https://owner.com (FPS are required to use the HTTPS scheme). The idea being that partitioning by First-Party Set mirrors top-level site partitioning, but behaves as if the top-level site was owner.com if the browser is visiting any site in owner.com's set (previously existing mechanisms like site for cookies won't do this, just cookie partitioning).

While the intention may be that the "owner" is fixed (just like the intention that a public suffix is fixed), I expect that in practice the values may change (e.g. if an "owner" domain is decommissioned/renamed).

One note to consider: Previously I mentioned that topLevelSite is automatically canonicalized. If an internal cookie is no longer stored in a canonical form, then the automatic canonicalization can prevent extension APIs from modifying that unreachable cookie. A way around this is to introduce a new property in the partitionKey object, e.g. a flag to signal that the input should not be canonicalized. This flag would be set on the cookies returned from the extension API.

Interesting. Is the idea that you would check each cookies' internal representation of their partition key when they're loaded from disk?

The internal representation is already used at some point. The cookie is stored with the canonical representation of the partition key (at the time of generation).

The extension API is already an abstraction of the internal representation of site.
URL -> site -> partitionKey -> look up in database.
In Firefox, the extension API currently uses a common method to convert the URL to a partitionKey, which normalizes the input if needed (in particular, eTLD+any becomes eTLD+1). If the definition of "eTLD" (public suffix) changes, then this means that extensions won't be able to read or write cookies given the partitionKey. Extensions may still see the cookie if querying all cookies with cookies.getAll. To support cookies.get/cookies.set/cookies.remove for cases where the database entry's representation is not canonical (i.e. unreachable by web content in practice), the extension API could set a special flag on the returned partitionKey, and accept an equivalent flag in the input partitionKey as a signal that the input partitionKey should not be normalized further.

Given the pending resolution of #40, perhaps we should revisit how we define the partitionKey argument here? It seems possible to add a boolean field alongside topLevelSite, in order to represent the same-site-ness of the partition.

That sounds fine off-hand but I'd leave it to @DCtheTall to have an informed opinion on this. Given the increasing stabilization / adoption of CHIPS and hopefully eventual standardization I agree that it would be nice to make some progress on this.

@bvandersloot-mozilla I agree that adding a boolean to the partitionKey struct is a good call. I was thinking something like hasCrossSiteAncestor, does that sound reasonable?

I think we will probably not implement this until we have finished adding the ancestor chain bit to the cookie partition key in Chrome, which is an active work in progress :)

Something like that sounds reasonable. It would be good to get some input from @Rob--W, and maybe inclusion in the explainer of how this extension API integration should work.

Even before the ping I was already watching the comments here with interest. The request for the extra field came up before at https://bugzilla.mozilla.org/show_bug.cgi?id=1873418 (comment 2 onwards). Given the explicit request for my input, I will first give a short recap of the shape of the current partitionKey API, and then sketch a proposal of a change to support the cross-site/same-site ancestor bit in the cookies extension API.

partitionKey in cookies API

The partitionKey object in the extension API was explicitly designed to support future/different additions. Whenever a new property is added, we have to ask what the desired default behavior should be when it was not specified in the partitionKey passed to the following methods:

  • get/set/remove
  • getAll

When partitionKey is omitted , the API operates on unpartitioned cookies only (equivalent to partitionKey: { topLevelSite: "" }). In get/set/remove, an empty object is equivalent to the object having been omitted, i.e. unpartitioned cookies are queried/modified. This design ensures that extensions without awareness of partitioning will not accidentally mix cookies from different partitions.
The cookies.getAll method's behavior differs when partitionKey is set without the topLevelSite field: in that case, rather than selecting unpartitioned storage like the other cookies methods, it selects any partition. This is a design aspect that enables getAll to be used to select some, all, or no partitions.

cross-site ancestor bit in partitionKey

Now, with that having said, the question is what the semantics of the new field should be. It looks like it is a boolean field, and the default seems to be false. Under these circumstances, I would propose the following API:

The cookies.Cookie type (describing the return values of the cookies API methods) gets an extra field in its partitionKey, the name yet to be determined ("hasCrossSiteAncestor" was suggested before). The value should always be populated by the browser with the correct value, when partitionKey is not null. Note: partitionKey is null for unpartitioned cookies; this implies that unpartitioned cookies cannot have the ancestor bit set for unpartitioned cookies. If this assumption is not true, a solution is to use partitionKey: { topLevelSite: "", hasCrossSiteAncestor: true } to set it apart from unpartitioned cookies.

When the field is set: get/set/remove/getAll operates on cookies with a matching ancestor bit.

When the field is not set:

  • get/set/remove: operate on cookies without the ancestor bit set
  • getAll: select cookies with the ancestor bit set or unset (may therefore return two cookies that are identical other than the value of the ancestor bit)

Submitted a proposal covering the inclusion of hasCrossSiteAncestor in extensions.