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

[FireMonkey] GM.fetch doesn't work when privacy.firstparty.isolate is true #431

Open
leonidborisenko opened this issue Feb 27, 2022 · 34 comments
Labels
addon: FireMonkey feature request 💡 New feature or request more feedback needed Extra attention is needed

Comments

@leonidborisenko
Copy link

Firefox has a "First-party isolation" feature. privacy.firstparty.isolate is a setting available in about:config.

GM.fetch fails when this feature is enabled. I traced failure to browser.cookies.getAll({url, storeId})call in addCookie() function defined in content/background.js.

According to cookies.getAll() API documentation, it has an optional parameter:

firstPartyDomain

A string representing the first-party domain with which the cookie to retrieve is associated.

This property must be supplied if the browser has first-party isolation enabled. You can however pass null in this situation. If you do this, then cookies with any value for firstPartyDomain, as well as cookies which do not have firstPartyDomain set at all, will be included in the results. See First-party isolation.

A little bit relevant issue in GreaseMonkey: greasemonkey/greasemonkey #2985

Current workaround (if sending cookies is not required) is to call:

await GM.fetch(url, {anonymous: true});
@erosman
Copy link
Owner

erosman commented Feb 27, 2022

I can pass firstPartyDomain which would work whether privacy.firstparty.isolate is true or false. It would eliminate the error, but I am not sure how the isolation will be affected. e.g.

const cookies = await browser.cookies.getAll({url, storeId, firstPartyDomain: null});

Update:
Done in v2.45

Feedback needed ....

@erosman erosman added more feedback needed Extra attention is needed done ✓ Completed bug 🐞 Something isn't working labels Feb 27, 2022
@leonidborisenko
Copy link
Author

leonidborisenko commented Mar 1, 2022

I didn't receive new version through updates yet. But I have some thoughts about fix to share.

I was heavily leaning on your understanding of First-party isolation. Honestly, event after reading in various sources about it, I still don't fully understand its' mechanism. Exactly the same as, I believe, you are.

But I think, that passing null as firstPartyDomain totally circumvents this feature. So it should fix the reported bug, but for the (probably unexpected) price of disabling isolation.

I suggest to revert this behavior. Let cookies.getAll fail by default if First-party isolation is enabled, but provide user (and possibly script author) with means to statically and/or dynamically override this behavior.

New global setting

As First-party isolation is important to extension user, ability to make decision about set of cookies to pass with GM.fetch must be available for extension user.

So there could be several layers, where firstPartyDomain parameter to cookies.getAll() is produced/consumed:

  • addCookies() function just consumes some cookies.getAll() details (firstPartyDomain amongst them) provided by extension user. If no such details are provided, firstPartDomain (and some other details) are just not set.
  • extension user must provide (in separate textarea in global settings) JavaScript module which exports:
    • either JavaScript function receiving url and storeId as parameters and returning an object that will be merged into details passed to cookies.getAll(url, storeId)
    • or JavaScript object with keys in form of [url, storeId] and values in form of object that will be merged into details passed to cookies.getAll(url, storeId)

Or maybe just function would be enough. Difference between object and function is the difference between static and dynamic decision. But function can just return static object (independently of function parameters).

Object returned by user should be sanitized, so that only firstPartyDomain and partitionKey (see below) are merged to details parameter of cookies.getAll().

Script author should not be able to (unobservably) overcome First-party isolation or conform with it

In this case script author doesn't have any power over isolation and all scripts with GM.fetch will fail for users with First-party isolation enabled and absent setting of that JavaScript module.

I think it's for better even if it pushes considerate chunk of work to extension user.

If this restriction for script authors should be lifted, in my opinion, it should not be done by allowing to pass parameters to cookies.getAll() from userscript (dynamically), because it's not easily observable by userscript user.

Granting to script author ability to conform with First-party isolation along with transparency of this conforming to user and providing extension user to easily observe and change (restrict) script behavior could be achieved by new @grant metadata entry with JSON object as parameter (with [url, storeId] as keys and susbset of cookie.getAll() details object as values).

For example,

// @grant requestCookies {["https://example.com", "firefox-private"]: {firstpartyDomain: "https://example.com", partitionKey: {topLevelSite: 'https://example.com'}}

It should be overridible by user in User Metadata.

Considering Dynamic state paritioning

According to cookies API / Tracking protection:

Firefox includes features to prevent tracking. These features separate cookies so that trackers cannot make an association between websites visited. So, in the preceding example, ad-tracker.com cannot see the cookie created on a-news-site.com when visiting a-shopping-site.com. The first iteration of this protection was first-party isolation which is now being superseded by dynamic partitioning.

Note: First-party isolation and dynamic partitioning will not be active at the same time. If the user or an extension turns on first-party isolation, it takes precedence over dynamic partitioning. However, when private browsing uses dynamic partitioning, normal browsing may not be partitioning cookies. See Status of partitioning in Firefox, for details.

I think, it means, that extension user should be able to control two (and only two) entries in details objects passed to cookies.getAll(): firstPartyDomain and partitionKey.

@erosman
Copy link
Owner

erosman commented Mar 1, 2022

I didn't receive new version through updates yet.

v2.45 is not released yet.

firstPartyDomain can be set as:

  • firstPartyDomain: ''
    This is the default value when privacy.firstparty.isolate is false but in FireMonkey it is limited to contextual identity
  • firstPartyDomain: null
    Returns all the cookies` but in FireMonkey it is limited to contextual identity
  • firstPartyDomain: 'example.com'
    I need to ask Mozilla engineers about the difference

@leonidborisenko
Copy link
Author

In implementing support for first-party isolation, please, consider concrete use-case: enabling "[v] Always use private browsing mode" on about:preferences#privacy page.

In this case contextual identity (storeId parameter of cookies.getAll) will always be represented as firefox-private while containers will not work because of Private browsing.

See:

Even though passing firstPartyDomain: null will still mean "Returns all the cookies' but in FireMonkey it is limited to contextual identity", in reality it will be the same as "Returns all cookies".

I might be wrong. Sorry for distraction then.

If you'll use the method of providing user-defined JavaScript function, then I want to note, that I didn't really fully think about set of parameters of this function that will be useful for extension user to make decision. It might be useful to additionally pass new URL(document.location), metadata block of user script etc.

@erosman
Copy link
Owner

erosman commented Mar 1, 2022

Please note that AFA Firefox contextual identities, Private browsing is a type of contextual identity (e.g. a special type of container), as mentioned under @container in FireMonkey.

The current situation is:

  • Normal Tab ➜ cookieStoreId = firefox-default ➜ cookies.getAll() ➜ is not run and FM lets browser handle it
  • Private Tab ➜ cookieStoreId = firefox-private ➜ cookies.getAll() ➜ private cookies for that tab URL only which should be session cookies as the rest are deleted
  • Container-1 Tab ➜ cookieStoreId = firefox-container-1 ➜ cookies.getAll() ➜ firefox-container-1 only cookies for that tab URL only
  • Container-2 Tab ➜ cookieStoreId = firefox-container-2 ➜ cookies.getAll() ➜ firefox-container-2 only cookies for that tab URL only
  • .... etc

I am not sure what effect would setting firstPartyDomain have on above (in addition to cookieStoreId).

PS. I changed the setting to '' for now.

const cookies = await browser.cookies.getAll({url, storeId, firstPartyDomain: ''});

@erosman
Copy link
Owner

erosman commented Mar 1, 2022

Test

const url = 'https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch';
const storeId = 'firefox-default';
browser.cookies.getAll({url, storeId});                                     // Array [ {…} ]
browser.cookies.getAll({url, storeId, firstPartyDomain: null});             // Array [ {…} ]
browser.cookies.getAll({url, storeId, firstPartyDomain: ''});               // Array [ {…} ]
browser.cookies.getAll({url, storeId, firstPartyDomain: 'mozilla.org'});    // Array []
const url = 'https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch';
const storeId = 'firefox-private';
browser.cookies.getAll({url, storeId});                                     // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: null});             // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: ''});               // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: 'mozilla.org'});    // Array []
const url = 'https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch';
const storeId = 'firefox-container-1';
browser.cookies.getAll({url, storeId});                                     // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: null});             // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: ''});               // Array []
browser.cookies.getAll({url, storeId, firstPartyDomain: 'mozilla.org'});    // Array []

@leonidborisenko
Copy link
Author

I think, your tests should include case where url is an URL of some third-party which is loaded an set cookies when mozilla.org and some other first-party domain is visited by user. And then firstPartyDomain should be changed in tests to mozilla.org and that other first-party domain, but url should stay the same.

Please note that AFA Firefox contextual identities, Private browsing is a type of contextual identity (e.g. a special type of container), as mentioned under @container in FireMonkey.

Maybe not so relevant thoughts about distinctive feature of firefox-private container

Speaking as a FireMonkey user, yes, you are totally right. There is no need to introduce any additional support for Private browsing mode.

But speaking as a Firefox user, in broader point of view, Private browsing mode is: 1) a container that could be used without installing additional extensions and 2) a container that automatically deletes cookies etc, i.e. most of storage (if not all) before closing browser.

These are weak reasons, because installing FireMonkey means that other extensions also could be installed (Temporary Containers, Cookie AutoDelete or others).

Still, Private browsing mode is provided by browser developers and might be integrated in user experience and tested better than extensions.

Anyway, this is probably tangential to First-party isolation issues. My thoughts were wandering somewhere but I just don't want to delete this text.

First-party isolation

I think this feature was proposed in Firefox bug #65965. Quoting from it:

dbaron proposes the following, for separating 3rd party cookies between different 1st party sites:

The idea is this: instead of making local-storage, cookies, etc. stored per-domain of the page setting the cookie/storage (and a domain can only access its own), it could be stored by the pair of (domain of the toplevel page) * (domain of the page doing the setting/getting). In other words, a page on facebook.com inside an iframe when the URL bar displayed cnn.com could only access storage set in the same situation.

It seems like this could help with fixing some of the privacy issues related to third-party cookies and storage while avoiding the risks of breaking sites (other than the need to log in multiple times when you really did want the sharing).

Comment 35 on this bug says:

This bug was resolved by bug 1260931 (implemented the First Party Isolation feature)

What First-party isolation (FPI) does

FPI could be seen as an automatic "container" (or "sub-container" when actual container is used) with the name computed from hostname (and in some cases also from scheme and port). It contains at least third-party cookies.

Without FPI:

  • User visit github.com. It loads something from third-party tracking.invalid, which sets cookie.
  • Then user visit example.com. It also loads something from third-party tracking.invalid. tracking.invalid sees cookie which it set before (at visiting github.com).

With FPI:

  • user visit github.com. It loads something from third-party tracking.invalid, which sets cookie.
  • Then user visit example.com. It also loads something from third-party tracking.invalid. tracking.invalid doesn't see cookie which it set before (at visiting github.com), because github.com is not example.com

What it means for cookies.getAll

I think, without FPI all cookies, which are set by <some third-party origin> are returned when cookies.getall(<url of that some third-party origin>, storeId) is called.

Graphically:
-----------------------------
| <some third-party origin> |
-----------------------------
| its' cookies              |
-----------------------------

But with FPI there could be many distinct cookie sets for single <some third-party origin>.

Graphically:
----------------------------------
|  <first distinct first-party>  |
|  ----------------------------- |
|  | <some third-party origin> | |
|  ----------------------------- |
|  | its' distinct cookies 1   | |
|  ----------------------------- |
|---------------------------------

-----------------------------------
|  <second distinct first-party>  |
|  -----------------------------  |
|  | <some third-party origin> |  |
|  -----------------------------  |
|  | its' distinct cookies 2   |  |
|  -----------------------------  |
|----------------------------------

So passing firstPartyDomain computed from <first distinct firsty-party> to cookies.getAll(<url of some third-party origin>, storeId) will get its' distinct cookies 1 and will not get its' distinct cookies 2.

What it means for GM.fetch

Passing null or '' as firstPartyDomain weakens expected (and explicitly enabled in about:config) isolation.

How FPI should be supported in FireMonkey

Well, now I honestly don't know. I might be the only user that is somehow crippled by not supporting FPI and for me {anonymous: true} workaround is just enough. So may be just let cookies.getAll fail (for now) when FPI is enabled and wait for another user to complain, so that use cases will be more clear and support of FPI could be designed more precisely.

What exactly first-party means

When I spoke about computing name of "container", I meant that it's not as simple as taking example.com from http://www.example.com:8081/index.html.

There are much more rules, defined in https://searchfox.org/mozilla-central/source/caps/OriginAttributes.cpp

For example, when privacy.firstparty.isolate.use_site is enabled in about:config, then "name of container" will be equal to (http,example.com,8081).

Link dump

These links contain bits of information about FPI

Comparison between FPI and Temporary Containers extension and also link provided in that page.

Consider changing the key of FPI to site (i.e., include the URL scheme) (implements privacy.firstparty.isolate.use_site).

Post on reddit about that setting.

Confused on first party isolation and FF85 network partitioning.

Test whether First Party Isolation works.

Resolved problems with FPI in Cookie AutoDelete extension: #1 and #2.

@erosman
Copy link
Owner

erosman commented Mar 2, 2022

Maybe not so relevant thoughts about distinctive feature of firefox-private container

It doesn't contradict what I said, although I meant from the browser API point of view of contextual identity e.g. the method cookies.getAll() deals with 'firefox-private'.

FPI & cookies.getAll in FireMonkey

  • User visit github.com. It loads something from third-party tracking.invalid, which sets cookie.
  • Then user visit example.com. It also loads something from third-party tracking.invalid. tracking.invalid sees cookie which it set before (at visiting github.com).

AFA I understand the situation, FPI relates mostly to the browsing behaviour which is not the same for FM HTTP request behaviour. Based on the above scenario:

  • On github.com, cookies will exist for github.com & tracking.invalid
  • On example.com, cookies will exist for example.com & tracking.invalid

FM HTTP request to target.com & Contextual Identities

  • On github.com in normal tab, FM lets Firefox handle cookies to target.com
  • On github.com in private tab, if exists, FM sends cookies created by target.com in private tab to target.com (I don't expect there to be any)
  • On github.com in container tab, if exists, FM sends cookies created by target.com in that same container to target.com

Also .... Enable extensions to send network requests (fetch) with a specific cookieStoreId (container tab context)

@leonidborisenko
Copy link
Author

leonidborisenko commented Mar 2, 2022

Overall, I agree that script author shouldn't be restricted in using HTTP requests basing on browsing behavior.

But in this situation user expectations should be also considered.

FM HTTP request to target.com & Contextual Identities

  • On github.com in normal tab, FM lets Firefox handle cookies to target.com
  • On github.com in private tab, if exists, FM sends cookies created by target.com in private tab to target.com (I don't expect there to be any)
  • On github.com in container tab, if exists, FM sends cookies created by target.com in that same container to target.com

In my current understanding of containers and first-party isolation, theirs' expected outcomes are mostly the same. Both are providing an element to the set of "origin attributes". When browser looks for data by origin, it also uses these origin attributes.

(These examples were edited, because I was using example.com both as first-party domain and origin which is meaningless for first-party isolation)

  • Without container and first-party isolation origin is: "https://example.com".
    For cookies.getAll it's cookies.getAll({url: "https://example.com"}).

  • In container (and private browsing mode), origin is "modified" by appending container origin attribute: "https://example.com^firefox-private".
    For cookies.getAll it's cookies.getAll({url: "https://example.com", storeId: "firefox-private"}).

  • When first-party isolation is enabled (but container is not used),
    origin is "modified" by appending computed first-party origin attribute: "https://example.com^firstparty.invalid".
    For cookies.getAll it's cookies.getAll({url: "https://example.com", firstPartyDomain: "firstparty.invalid"}).

  • When first-party isolation is enabled and container is used,
    origin is "modified" by appending computed first-party origin attribute and container origin attribute: "https://example.com^firstparty.invalid^firefox-private".
    For cookies.getAll it's cookies.getAll({url: "https://example.com", firstPartyDomain: "firstparty.invalid", storeId: "firefox-private"}).

(Resulting strings of "modified" origin are not the exact representations, but they are close enough to illustrate the described behavior.).

So if FireMonkey limits access to cookies basing on storeId (on container contextual identity), the same logic should be applied to firstPartyDomain.

I want to stress again, that firstPartyDomain value is computed (and not just equals to second-level domain; at least, not in all cases).

Not using firstPartyDomain could break scripts (if user enable first-party isolation), but it doesn't leak cookies in that case. As I'm the only one who report this script breaking and I can live with it, practically not using firstPartyDomain doesn't break anything.

Though using '' or null as firstPartyDomain is an easy fix, it breaks browser user expectation (because it unexpectedly leaks cookies). It's also an exception from FireMonkey respecting "origin attributes" in form of container contextual identities (because passing '' is like storeId: <give me cookies not from this container, but from normal tab> and passing null is like passing storeId: <give me cookies from all containers and normal tab>, if such possibilities existed).

@erosman
Copy link
Owner

erosman commented Mar 2, 2022

I am not sure the example covers the situation.

TAB that userscript inserted into

  • URL: https://example.com/
  • Contextual Identity of the Tab: 'default|private|container-N`
  • firstPartyDomain: example.com

Userscript GM HTTP Request from above tab

  • URL: target.com
  • Cookies sent to above: belonging to target.com (not example.com)
  • Cookies sent to above: match the Contextual Identity of the TAB they are sent from
  • firstPartyDomain: example.com is irrelevant to cookies sent via GM HTTP Request to target.com

@leonidborisenko
Copy link
Author

Userscript GM HTTP Request from above tab

  • URL: target.com
  • Cookies sent to above: belonging to target.com (not example.com)
  • Cookies sent to above: match the Contextual Identity of the TAB they are sent from
  • firstPartyDomain: example.com is irrelevant to cookies sent via GM HTTP Request to target.com

Given that first-party isolation is disabled, firstPartyDomain is irrelevant in cookies.getAll. But it's not the case for which issue was raised. And when first-party isolation is disabled, there is no issue, because then cookies.getAll doesn't fail.

Why storeId is passed in cookies.getAll call?

Given that first-party isolation is enabled by user, why is Contextual Identity is relevant, but firstPartyDomain is irrevlevant?

When user creates new container, he expects that cookies from other containers will not leak into current (new) container. Passing storeId satisfies this expectation.

Maybe storeId is used just for single practical reason: because cookies cannot be passed to requested URL implicitly by browser (as HTTP request is made from extension context and not from container context), so cookies should be retrieved explicitly and so storeId is required, because otherwise cookies will be got from extension cookie jar (from wrong cookie jar).

But still passing storeId provides privacy benefits.

When user enables first-party isolation and loads example.com in new tab, he expects that cookies set by target.com in other tabs with different first-party domains will not not leak into current tab. Passing firstPartyDomain to cookies.getAll satisfies this expectation.

Note that userscript acts on behalf of user, not on behalf of script author.

Script author doesn't care about what cookies are passed to URL requested from userscript. Script author doesn't care about user privacy. User (with enabled first-party isolation) cares about own privacy a lot.

So if firstPartyDomain will not be passed to cookies.getAll, it will fail when first-party isolation is enabled. Userscript will not work and user will be angry. But his privacy will be kept and he can choose between disabling first-party isolation, asking script author to use {anonymous: true} in requests from script or stopping script usage.

If firstPartyDomain will be computed by FireMonkey and passed to cookies.getAll, user expecttions from first-party isolation will be satisfied.

But if '' or null will be passed as firstPartyDomain, user expectations from enabling first-party isolation will be silently broke by FireMonkey and user privacy could be breached.

@erosman
Copy link
Owner

erosman commented Mar 2, 2022

I have been asking Mozilla engineers for verification. Here is my latest query:

When making an HTTP request to target.com, from a tab with url of exmaple.com, what would be relevance of firstPartyDomain to the XHR to target.com?

The first party isolation should relate to exmaple.com.
It should not have any bearing on the cookies sent to target.com, should it?

📌 From a different angle, when making XHR to target.com & manually getting cookies to match storeId of the generating tab, what should the value for firstPartyDomain be set as, so that it covers both the users who have turned it on as well as those who have it off?

ref: Bug 1670278

@erosman
Copy link
Owner

erosman commented Mar 3, 2022

If you know what to set firstPartyDomain as, let me know and I will update the code accordingly.

browser.cookies.getAll({url, storeId, firstPartyDomain: ''}); 

@leonidborisenko
Copy link
Author

Revert it to

browser.cookies.getAll({url, storeId});

and leave it be. Otherwise, look at Firefox code for the right answer.

Exact value of firstPartyDomain must be computed at runtime from URL of location loaded in browser tab, where userscript is executed, and according to algorithm, defined in Firefox source code at caps/OriginAttributes.cpp in PopulateTopLevelInfoFromURI function (see it in current version at line 53).

There are two such algorithms (first is a subset of second):

  • one is applied if just privacy.firstparty.isolate is enabled
  • but if privacy.firstparty.isolate and privacy.firstparty.isolate.use_site are both enabled, then after applying first algorithm, the result of it is proccesed further

For example, in case when https://example.com is loaded in browser tab:

  • firstPartyDomain must be example.com if just privacy.irstparty.isolate is enabled
  • firstPartyDomain must be (https,example.com) if privacy.firstparty.isolate and privacy.firstparty.isolate.use_site are both enabled

(But algorithm is not so simple as: just take second-level domain. At least not in all cases.)

If privacy.isolate.firstparty is disabled, firstPartyDomain must not be passed to cookies.getAll at all.

@erosman
Copy link
Owner

erosman commented Mar 3, 2022

Revert it to

No problem ...

For example, in case when https://example.com is loaded in browser tab:

That doesn't relate to target.com which is where the cookies are for.

Here is the process:

  • Tab: https://example.com in storeId
  • GM.fetch('target.com'):
  • browser.cookies.getAll({url: 'target.com', storeId});
  • Send target.com cookies to target.com

Where does https://example.com relate to getting cookies for target.com?
The only thing the code gets from the tab is the storeId, so that it maintains the same contextual identity.

@leonidborisenko
Copy link
Author

Where does https://example.com relate to getting cookies for target.com?

First-party isolation is a tracking protection. It matters when target.com is an agent collecting data about user behavior. And there should be at least two different first-parties to see how using firstPartyDomain provides protection.

Without first-party isolation:

  • when I visit first-site.com and it makes a request to third-party target.com, target.com can set unique cookie that identifies me precisely.
  • when I visit second-site.com and it makes a request to third-party target.com, target.com receives that cookie that is set when i visited first-site.com. It can track me.

But with first-party isolation, when I visit second-site.com and it makes request to third-party target.com, browser doesn't send cookies that were stored when I visited first-site.com. There are two isolated cookie jars for first-site.com and second-site.com.

Then. when I'll visit third-site.com, browser will use third isolated cookie jar etc.

The same logic must apply to userscript request made on behalf of user in context of concrete tab. Otherwise it just doesn't meet expectations and guarantees that first-party isolation satisfies and provides.

Speaking bluntly, passing '' or null as firstPartyDomain just breaks first-party isolation and exfiltrate cookies that must be inaccessible.

Let me describe an example in slightly changed way:

  • I start with disabled first-party isolation
  • I visit 1st-site.com, it embeds iframe of target.com, target.com sets unique cookie
  • I enable first-party isolation
  • I visit 1st-site.com, it embeds iframe of target.com. target.com doesn't receive cookie that was set when first-party isolation was disabled. target.com sets unique cookie, but it's isolated in separate cookie jar.
  • Afterwards I visit 1st-site.com again, target.com receives that unique cookie from isolated cookie jar, but it doesn't matter, because first-party isolation effect is revealed in case of at least two different first-parties
  • Afterwards I visit 2nd-site.com, it embeds iframe of target.com, target.com doesn't receive that unique cookie that was set when I visited 1st-site.com.
  • Afterwards I visit 3rd-site.com, userscript makes request to target.com, FireMonkey happily sends all cookies from all isolated cookie jars (if firstPartyDomain is null) or cookies that were set when first-party isolation was disabled (if firstPartyDomain is ''). target.com can track me.

@erosman
Copy link
Owner

erosman commented Mar 3, 2022

OK... how can the desired outcome be achieved?

  • browser.cookies.getAll({url: 'target.com', storeId}); is the same as browser.cookies.getAll({url: 'target.com, storeId, firstPartyDomain: ''}) when first-party isolation is false BUT fails when it is true
  • browser.cookies.getAll({url: 'target.com, storeId, firstPartyDomain: 'target.com'}) results in an empty array

So, what shall be done in GM.fetch to cater for everyone?

@leonidborisenko
Copy link
Author

Value of url parameter doesn't have any influence in finding required value of firstPartyDomain.

  1. Determine from extension whether privacy.firstparty.isolate is enabled or not
  2. If privacy.firstparty.isolate is not enabled, don't pass anything as firstPartyDomain (nor even '', neither null).
  3. If privacy.firstparty.isolate is enabled, determine from extension whether privacy.firstparty.isolate.use_site is enabled or not
  4. If privacy.firstparty.isolate.use_site is not enabled, compute firstPartyDomain value by algorithm, defined in Firefox source code at caps/OriginAttributes.cpp in PopulateTopLevelInfoFromURI function (see it in current version at line 53) and pass it as firstPartyDomain. When computing:
    • aURI is the URL in location bar of tab where userscript is executed
    • select branches assuming aUseSite is false.
  5. If privacy.firstparty.isolate.use_site is enabled, execute previous step, but in algorithm of computing firstPartydomain select branches assuming aUseSite is true.

@erosman erosman added feature request 💡 New feature or request and removed bug 🐞 Something isn't working done ✓ Completed labels Mar 4, 2022
@erosman
Copy link
Owner

erosman commented Mar 4, 2022

Parameters in PopulateTopLevelInfoFromURI are not available to extensions.

Since privacy.firstparty.isolate, privacy.firstparty.isolate.use_site are false by default, let's see how they work out when they become true by default and standard feature of Firefox.

@leonidborisenko
Copy link
Author

leonidborisenko commented Mar 5, 2022

erosman added (feature request) and removed (bug) (done) labels

Can you describe what it means in terms of code? All changes made for "supporting" first-party isolation are reverted and now browser.cookies.getAll (called by FireMonkey background script) again just fails when 'privacy.firstparty.isolate' is enabled, right?


Since privacy.firstparty.isolate, privacy.firstparty.isolate.use_site are false by default, let's see how they work out when they become true by default and standard feature of Firefox.

They will not be enabled by default in Firefox ever. However, dynamic first-party isolation (state partitioning + total cookie protection) is already exposed through about:preferences#privacy. It's enabled when "Enhanced tracking protection" is set to "Strict" (see resolved bugzilla bug #1686296).

There is an extensive discussion of what dFPI is in arkenfox/user.js#1051.

Citation from arkenfox/user.js#1051 (comment)

Thorin-Oakenpants commented on Mar 29, 2021

Off the top of my head in general terms

  • FPI covered everything (networking things, content, permissions, storage, persistent web storage etc) by assigning origin attributes
  • FPI's replacement isolates by partyness into two distinct groups
    • in both groups the origin attributes are and can be hardened (double keyed, +scheme)
    • these two groups are not the same
    • total cookie protection (TCP) - basically persistent web storage and a few other related items
      • TCP is the one that can be changed by users (used in ETP strict mode which does extra things as well)
      • TCP is the one that where the partyness can be dynamic (see dFPI)
      • dFPI (the term has been misused in a few places including here in the early days) is so that the origin attributes can be relaxed e.g. via user actions e.g. click a login button that connects to google, it relaxes some things for google for that first party in that session
    • network partitioning - everything else: connections, requests, cache, favicons, dns, blah blah .. everything that can always be isolated by first party and doesn't break websites
      • network partitioning is on by default and doesn't change partyness AFAIK - there is nothing dynamic about this

That's my understanding.

Note: AFAICT .. the term dFPI is/was a bit misunderstood/misused as the overall FPI replacement name. dFPI is really only about one group (TCP), not the whole FPI replacement

Dynamic first-party isolation is related to {partitionKey: ...} and {partitionKey: {topLevelSite: ...}} parameters of browser.cookies.getAll. And topLevelSite should be computed by algorithm defined in the same PopulateTopLevelInfoFromURI (see line 179 of current caps/OriginAttrbutes.cpp).


Parameters in PopulateTopLevelInfoFromURI are not available to extensions.

Of course, they are not. But their values can be deduced and either ignored (as irrelevant), or got by extension through standard APIs, or requested by extension from user (providing knobs in FireMonkey settings).

  1. aIsTopLevelDocument, aIsFirstPartyEnabled and aForced are only used in computing condition used for decision of early return from function. They should be ignored while early return should be just completely skipped.
  2. aOriginAttributes and OriginAttributes::*aTarget is C++-specific method of passing pointer to object member (by passing pointer to class instance and pointer to class member). They are used only once in function in combination for setting variable topLevelInfo which is the actual pointer to object member. Now, topLevelInfo is used as a target for the result of algorithm. So aOriginAttributes and OriginAttributes::*aTarget should be ignored and using topLevelInfo should be interpreted as returning result of PopulateTopLevelInfoFromURI.
  3. In case of first-party isolation:
    • aURI is the URL in location bar
    • aIsFirstPartyEnabled is value of privacy.firstparty.isolate
    • aUseSite is value of privacy.firstparty.isolate.use_site

It might be productive to reference this issue and ask Mozilla to expose algorithm of computing firstPartyDomain and partitionKey.topLevelSite (defined in caps/OriginAttributes.cpp) through WebExtension APIs.

@erosman
Copy link
Owner

erosman commented Mar 5, 2022

Can you describe what it means in terms of code? All changes made for "supporting" first-party isolation are reverted and now browser.cookies.getAll (called by FireMonkey background script) again just fails when 'privacy.firstparty.isolate' is enabled, right?

Yes, I reverted the code back to what it was as per #issuecomment-1058365410.

I have already asked Mozilla engineers for the proper way to set firstPartyDomain in cookies.getAll for the purpose of HTTP Request, but was referred to the MDN on First-party isolation.

I am happy to implement the feature, and open to suggestions on how to actually implement it (i.e. the code).

@leonidborisenko
Copy link
Author

Yes, I reverted the code back to what it was

Thank you.

I have already asked Mozilla engineers for the proper way to set firstPartyDomain in cookies.getAll for the purpose of HTTP Request, but was referred to the MDN on First-party isolation.

They pointed in right direction, but in my opinion that information on MDN is insufficient to find how to determine required values in different contexts of multiple sites which URLs will be known only after user browsed to them.

I am happy to implement the feature, and open to suggestions on how to actually implement it (i.e. the code).

I believe I can try to provide you with translation of C++ function to JavaScript function (and supporting JavaScript code) in form of patch to FireMonkey code.

But there is a licensing issue. As I understand, you distribute FireMonkey source code only through addons.mozilla.org and you're fine with people looking at it. But what about modifying it and contributing modifications? Can you put a LICENSE file (with text of license for your source code) in distributed extension extension on next release?

A bigger issue with licensing is that translation of algorithm from Firefox C++ source code to JavaScript code will be a derivative work and it should be attributed so and licensed as Firefox C++ code. Is Firefox code license compatible with your extension code license? The only way to be sure for me is to read LICENSE explicitly distributed by you along with FireMonkey source code.

To put it straight: it's possible that I will not be able to contribute a patch even after including LICENSE, but in absence of LICENSE compatible with Firefox source code, I will certainly not contribute it.

It's not an ultimatum, I'm fine with current situation. Just trying to make this situation clear.


But there could be an easier way (if only taking more time to get through) without any licensing issues.

Try to refer to this issue and ask Mozilla to implement browser.cookies.getOriginAttributes(urlInLocationBar, thirdPartyUrl) function that will use functions from caps/OriginAttributes.cpp and return {firstPartyDomain: ..., {partitionKey: {topLevelInfo: ...}} according to current status of privacy.firstparty.isolate, privacy.firstparty.isolate.use_site, Total cookie protection and privacy.dynamic_firstparty.use_site. When privacy.firstparty.isolate is disabled, there should be no firstPartyDomain in returned object. When Total cookie protection is disabled, there should be no paritionKey in returned object.

In this case you will be able to just merge this returned object to details of browser.cookies.getAll.

@erosman
Copy link
Owner

erosman commented Mar 5, 2022

But what about modifying it and contributing modifications?

As per AMO page, FireMonkey is distributed under Mozilla Public License 2.0 Open Source Licence.

Getting privacy values require privacy permission and users are really concerned about new permissions (they even complain about harmless ones), especially when the situation only applies to a select group. It that case, often the right approach is to ascertain the popularity & demand.

BTW, have you tested the actual result with privacy.firstparty.isolate true vs false, as per #431 (comment) ?
In my test, there was no difference.

Is there any other userscript manager or other addon that is implementing a similar feature?

@erosman
Copy link
Owner

erosman commented Mar 7, 2022

I have been thinking about this. TBH, it should not be necessary to use an algorithm to deduce values for in firstPartyDomain in cookies.getAll(). Since it is a setting in cookies.getAll(), then there should also be a standard method to get a value for it, and there should also be guidance on it.

A setting that no one knows what value it should have, is no help.

@leonidborisenko
Copy link
Author

leonidborisenko commented Mar 7, 2022

I noted your thoughts, but I will answer to your comments sometime later, because for now I want to discuss patch workflow.

I've created a gist (https://gist.github.com/leonidborisenko/5f38c1cd3b38404fa5979dfc05516c37) with work-in-progress patch and instructions on applying this patch. It just contains algorithm translated from C++, but doesn't do anything useful. completed patch (work had been finished at 2022-03-15).

I will update gist in process of further work and post update pings here. Is this workflow suitable for you?

@erosman
Copy link
Owner

erosman commented Mar 7, 2022

Where is ./vendor/index.js?

Some parts are not necessary for FireMonkey since:

  • if (scheme === 'about')
    only about:blank is possible, not other about:
  • if (scheme === 'moz-extension')
    Not applicable to userscripts
  • Ports are not relevant in context of userscripts re: Match patterns/Globs

@erosman
Copy link
Owner

erosman commented Mar 14, 2022

J have been talking with Mozilla engineers and .........

erosman

firstPartyDomain

When getting cookies in order to pass via an HTTP request to target.com, from a tab with url of exmaple.com, what value should be set for firstPartyDomain?

While I understand the function of firstPartyDomain in browsing (getting/storing cookies), its function in XHR is unclear.

The mentioned values of '' or null both bypass firstPartyDomain and therefore defeat its purpose.

Reply

That field is only relevant if first party isolation is enabled, which is a nonstandard and unsupported feature hidden in aboutconfig

erosman

True ... I came across it since some users have it enabled. Tor browser also has it enabled.
With the privacy.firstparty.isolate enabled, browser.cookies.getAll throw an error which breaks the execution.

Therefore, I wanted to know how to set a value for it.

Reply

Ah okay. In your example it should be example.com

erosman

For my better understanding, how would example.com factor in an XHR to target.com?
Only cookies belonging to target.com are being queried and sent.

PS. The whole reason for this process is due to open Bug 1670278

Reply

the cookies would only be included if the firstPartyDomain also matches (that's how it ought to be anyway and roughly what we're doing for TCP)

erosman

How does that work?

Reply

if the Principal is target.com^firstPartyDomain=example.com and target.com sets a cookie, we'd use that Principal to store it and only if the Principal matches on a subsequent request would retrieval of the cookie work

Test

(() => {
const url = 'https://greasyfork.org/en';

browser.cookies.getAll({url})
.then(c => console.log('no-fpd', c))
.catch(console.log);

browser.cookies.getAll({url, firstPartyDomain: ''})
.then(c => console.log('empty-fpd', c))
.catch(console.log);

browser.cookies.getAll({url, firstPartyDomain: null})
.then(c => console.log('null-fpd', c))
.catch(console.log);

browser.cookies.getAll({url, firstPartyDomain: 'google.com'})
.then(c => console.log('domain-fpd', c))
.catch(console.log);
})();

Result

// privacy.firstparty.isolate: false
no-fpd      Array(3) [ {}, {}, {} ]
empty-fpd   Array(3) [ {}, {}, {} ]
null-fpd    Array(5) [ {}, {}, {}, {}, {} ]
domain-fpd  Array []

// privacy.firstparty.isolate: true
Error: First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set.
empty-fpd   Array(3) [ {}, {}, {} ]
null-fpd    Array(5) [ {}, {}, {}, {}, {} ]
domain-fpd  Array []

Besides the error in 2nd test with no-fpd, the results are the same.

Can you run the test (from FireMonkey Toolbox via about:debugging#/runtime/this-firefox -> Inspect) with different domains and see if the results are different since you have privacy.firstparty.isolate: true all the time?

@leonidborisenko
Copy link
Author

While I understand the function of firstPartyDomain in browsing (getting/storing cookies), its function in XHR is unclear.

When Firefox selects cookies for the purpose of sending them in HTTP(S) request initiated by Firefox itself in context of browser tab, it automatically computes and uses firstPartyDomain (when FPI is enabled).

But when HTTP(S) request is made from FireMonkey background page, Firefox doesn't know the exact tab where this request is actually started, so if FireMonkey wants to select cookies with providing first-party isolation guarantees, then firstPartyDomain must be computed manually.

When getting cookies in order to pass via an HTTP request to target.com, from a tab with url of exmaple.com, what value should be set for firstPartyDomain?

Reply
Ah okay. In your example it should be example.com

Reply
if the Principal is target.com^firstPartyDomain=example.com and target.com sets a cookie, we'd use that Principal to store it and only if the Principal matches on a subsequent request would retrieval of the cookie work

Do you understand why firstPartyDomain should be equal to example.com when URL in tab's location bar contains example.com?

For example, do you agree that when https://childcare-support.tax.service.gov.uk/par/app/trialmessage is loaded in tab, then firstPartyDomain parameter of browser.cookies.getAll for getting cookies to send in any HTTP(S) request made in context of this tab should be equal to:

  • tax.service.gov.uk (when privacy.firstparty.isolate is true)

  • or to (https,tax.service.gov.uk) (when privacy.firstparty.isolate is true and privacy.firstparty.isolate.use_site is true)?

(Because service.gov.uk is included in public suffix list.)

Do you understand why there are two domains in Principal and how first domain should be determined? Do you understand how Principal value is used by Firefox for selecting cookies from cookie jar to send in HTTP request? How Principal will look when FPI is disabled?

Without FPI Firefox uses only target.com (i.e. requested URL) to select cookies to send in HTTP request. With enabled FPI Firefox computes firstPartyDomain from URL in location bar and stores it along with every received cookie. Then, before HTTP request Firefox computes firstPartyDomain again (from URL in location bar) and sends only cookies that have matching firstPartydomain.

And computing firstPartyDomain doesn't mean "just take second-level domain", even though it looks like it in some cases.

When FPI is disabled, then firstPartyDomain is not required by browser.cookies.getAll at all. So any value of firstPartyDomain (either constant, or variable) passed to browser.cookies.getAll is meaningless for the purpose of providing first-party isolation (because FPI is disabled), even though you can get different results for different values and you can get valid behavior with some passed values. Therefore tests are meaningless in this case.

When FPI is enabled, then real first-party isolation is provided by getting cookies with firstPartyDomain dynamically derived from URL in location bar. So there could not be constant value for firstPartyDomain. But to observe effect of FPI you should test with at least two different first-parties and common third-party and some third-party cookies should be actually set (and stored) before calling browser.cookies.getAll.

And with this, I believe, there is no common ground between us to discuss results of your tests. While I did describe my understanding of FPI mechanism and scenarios when FPI effect can be observable, you didn't. You can find mistakes in my reasoning without me even performing any tests, but I can't find any mistakes in your reasoning, because you didn't provide any reasoning that your tests were founded on.

It's not a fault per se, but if you want discussion, then I just struggle to understand:

  • What actions did you perform before tests to create test environment? If you did perform any such actions, were these actions required? Why? If you didn't perform any such actions, then did you take into account all consequences of inaction (cookies, set in tests with disabled FPI, can leak into results of tests with enabled FPI and firstPartyDomain: ''; enabled "Total cookie protection" can influence results of tests with disabled FPI; etc.)?

  • Why did you choose these exact values to pass to browser.cookies.getAll?

  • What results did you expect to see? Why?

  • Were results of your tests the same as expected results?

  • If results differed from expected, then how do you explain differences?

@leonidborisenko
Copy link
Author

I've finished patch. There is no need for additional permissions, because I use heuristics to determine enabled method of cookies isolation.


As per AMO page, FireMonkey is distributed under Mozilla Public License 2.0 Open Source Licence.

Thanks, I didn't notice that. Now I see that license is also mentioned in descripton on AMO. It's sufficient for me to provide a patch. But it would be nice to include text file with license in distributed XPI and mention name of license in short leading comment in source files.


I have been thinking about this. TBH, it should not be necessary to use an algorithm to deduce values for in firstPartyDomain in cookies.getAll(). Since it is a setting in cookies.getAll(), then there should also be a standard method to get a value for it, and there should also be guidance on it.

A setting that no one knows what value it should have, is no help.

Well, there is a standard method to get a value for firstPartyDomain: function in Firefox C++ code. But it's not exposed in WebExtension API, so at this moment it must be re-implemented.

Comment by Firefox contributor from ten months ago (https://bugzilla.mozilla.org/show_bug.cgi?id=1669716#c10):

  1. Parsing firstPartyDomain value from extension
    [...] Another issue is that it's not obvious to extensions that they have to compute the eTLD+1 of a tab's URL to use as the firstPartyDomain.

Getting privacy values require privacy permission and users are really concerned about new permissions (they even complain about harmless ones), especially when the situation only applies to a select group. It that case, often the right approach is to ascertain the popularity & demand.

I totally agree that requiring additional permissions is a no-go. And even privacy permission will not help with getting values of privacy.firstparty.isolate.use_site, network.cookie.cookieBehavior and network.cookie.cookieBehavior.pbmode.

Some heuristics can be used to select appropriate behavior and escape hatch can be provided in form of entry in FireMonkey preferences where user can explicitly select currently enabled method of cookie isolation.


Is there any other userscript manager or other addon that is implementing a similar feature?

No. Issue for Greasemonkey is linked in top post (greasemonkey/greasemonkey#2985). I've created an issue for Violentmonkey (violentmonkey/violentmonkey#1467). Tampermonkey is closed-source now and when it was open-sourced, there was no first-party isolation in Firefox.


Where is ./vendor/index.js?

It's in added-files.zip included in gist.


Some parts are not necessary for FireMonkey since:

I understand that FireMonkey requires only subset of functionality provided by re-implemented function. But I tried to translate function closer to original implementation, so that any new changes in C++ code could be easily mapped to translated code. I believe, it will help to support this code in future.

@erosman
Copy link
Owner

erosman commented Mar 14, 2022

Thank you for the explanation.

Therefore, it appears that AFA cookies.getAll is concerned the values of '', null, and tab hostname are not suitable for firstPartyDomain.

Please note that while host or hostname can be extracted from tab URL, getting the TLD is not possible without additional library.

@leonidborisenko
Copy link
Author

Please note that while host or hostname can be extracted from tab URL, getting the TLD is not possible without additional library.

My patch doesn't work without additional libraries and publicsuffixlist.js is one of them, being used to get "public suffix"/"eTLD" from tab's URL. All required libraries are included in added-files.zip (contained in gist with patch) and imported by ./vendor/index.js in patch. Licenses of these libraries are compatible with MPLv2.

@Baerbeisser
Copy link

Baerbeisser commented Dec 8, 2022

privacy.firstparty.isolate aka First-Party Isolation was added during the Tor-Uplift Project as Cross-Origin Identifier Unlinkability and was replaced in Firefox 77 by Dynamic First Party Isolation options in the settings. It was never intended to be used outside the Tor Browser and may leak additional trackable data if used with differing setups.

So, in short: You shouldn't have used it, don't use it anymore and don't complain if stuff breaks if you still do. Please ignore every 2nd privacy-enhancement website still telling you to do so.

@leonidborisenko
Copy link
Author

leonidborisenko commented Jan 19, 2023

So, in short: You shouldn't have used it, don't use it anymore and don't complain if stuff breaks if you still do. Please ignore every 2nd privacy-enhancement website still telling you to do so.

Thanks for your concerns. In fact, amidst discussion of this issue I did extensively research why FPI was implemented. I've summarized my research in gist (see for instance, some of its' sections: 1, 2, 3) linked in #431 (comment). So I am fully able to make my own decisions, whether to use it or not.

And I've not only complained about FireMonkey error, but also have provided a patch. Again, see aforementioned gist. I did all I could to minimize amount of work required from FireMonkey author to resolve issue.

While reported error doesn't occur when dFPI is used, underlying cause still affects dFPI users. Firefox doesn't automatically apply isolation to HTTP(S) requests made from extensions. It should be done manually by extension author. FireMonkey just blissfully ignores enabled FPI and enabled dFPI. It retrieves and sends cookies from unpartitioned storage when userscript makes request to third-party site.

Quote from Browser Extensions > JavaScript APIs > cookies # Storage Paritioning:

When using dynamic partitioning, Firefox partitions the storage accessible to JavaScript APIs by top-level site while providing appropriate access to unpartitioned storage to enable common use cases.
[...]
By default, cookies.get(), cookies.getAll(), cookies.set(), and cookies.remove() work with cookies in unpartitioned storage. To work with cookies in partitioned storage in these APIs, topLevelSite in partitionKey must be set. The exception is getAll where setting partitionKey without topLevelSite returns cookies in partitioned and unpartitioned storage.

Quote from Browser Extensions > JavaScript APIs > cookies > cookies.getAll() # Parameters

partitionKey (Optional)

An object defining which storage partitions to return cookies from:

  • if omitted, returns only cookies from unpartitioned storage.
  • if included without topLevelSite, returns all cookies from partitioned and unpartitioned storage.
  • if included with topLevelSite specified, returns cookies from the specified partition storage.

My patch implements isolation guarantee (as much as it's possible by using official WebExtension API) and resolves underlying cause for FPI and dFPI users. With this change, reported bug is also fixed.

@Baerbeisser
Copy link

@leonidborisenko: Sorry, my bad. I always tell people to only post after they read the whole thing... 🤦

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addon: FireMonkey feature request 💡 New feature or request more feedback needed Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants