Skip to content

Instantly share code, notes, and snippets.

@leonidborisenko
Last active March 15, 2022 15:24
Show Gist options
  • Save leonidborisenko/5f38c1cd3b38404fa5979dfc05516c37 to your computer and use it in GitHub Desktop.
Save leonidborisenko/5f38c1cd3b38404fa5979dfc05516c37 to your computer and use it in GitHub Desktop.
FireMonkey 2.44 patch (to support FPI and dFPI)
dummy file whose name will become gist name

Applying patch to FireMonkey

Unzip added-files.zip and copy all files from added-files directory to FireMonkey extension root directory.

Then apply firemonkey-2.44-fpi.patch.

fpi-dfpi-explanation.md

fpi-dfpi-explanation.md↓ contains link dump with information about FPI and dFPI.

PopulateTopLevelInfoFromURI.js

PopulateTopLevelInfoFromURI.js↓ contains JavaScript implementation of PopulateTopLevelInfoFromURI extracted from patch into separate JavaScript file.

It's meant for easier reviewing at web interface and, maybe, using in other projects.

vendor directory is contained in added-files.zip.

If there is any interest, it's licensed under the same license as Firefox code (Mozilla Public License, version 2.0).

Why there is a ZIP archive (added-files.zip)?

I planned to provide just a single patch file.

But my changes include added libraries. And resulting patch file is "overloaded" with added lines of new files. But I want to focus only on changes of existing FireMonkey source files.

So I've excluded added files from patch. And then tried to include them as separate files in gist.

These new files must be put in certain directory hierarchy. However, gist.github.com doesn't support directories. So I've zipped required directory hierarchy with new files and commited archive into Gist repository.

Contents of added-files.zip
added-files
└── content
    └── vendor
        ├── index.js
        ├── ip-regex
        │   ├── index.js
        │   ├── license
        │   └── upstream-info.txt
        ├── publicsuffix-list
        │   ├── public_suffix_list.dat
        │   └── upstream-info.txt
        ├── publicsuffixlist.js
        │   ├── publicsuffixlist.js
        │   ├── upstream-info.txt
        │   └── wasm
        │       ├── publicsuffixlist.wasm
        │       └── publicsuffixlist.wat
        └── punycode.js
            ├── LICENSE-MIT.txt
            ├── punycode.js
            └── upstream-info.txt

Compatibility of Mozilla Public License (version 2.0)

Regarding compatibility between FireMonkey license (MPLv2.0) and licenses of added libraries (along each added library license is provided: either in leading comment inside of source file, or in separate text file).

https://www.mozilla.org/en-US/MPL/license-policy/

Licenses Compatible with the MPL

The following waivers and licenses are compatible with the Mozilla Public License, version 2.0, in the sense that code under them can be included in the same binary:

  • Creative Commons Zero
  • Other Public Domain dedications
  • MIT, New BSD, and similar permissive licenses
  • Apache 2.0
  • GPL and MPL dual license

In addition, it may be permissible to import Third Party Code under the LGPL (version 2.0 upwards) to be Product Code if it's a clearly-demarcated library and will be dynamically linked into the product.

The following licenses are not compatible with the Mozilla Public License, version 2.0:

  • CC-BY, CC-BY-*
  • GPL
diff --git a/content/background.js b/content/background.js
index 8861..7d70 100644
--- a/content/background.js
+++ b/content/background.js
@@ -1,4 +1,5 @@
import {pref, App, Meta, RemoteUpdate, CheckMatches} from './app.js';
+import {ipV4Regex, ipV6Regex, publicSuffixList} from './vendor/index.js';
const RU = new RemoteUpdate();
// ----------------- Context Menu --------------------------
@@ -787,24 +788,387 @@ class API {
.catch(error => App.log(name, `${message.api} ➜ ${error.message}`, 'error')); // failed notification
case 'fetch':
- return this.fetch(e, storeId);
+ return this.fetch(e, storeId, sender.tab.url);
case 'xmlHttpRequest':
return this.xmlHttpRequest(e, storeId);
}
}
- async addCookie(url, headers, storeId) {
+ // Here follows translation of C++ method `MakeTopLevelInfo`
+ // defined in `caps/OriginAttributes.cpp` file in Firefox sources:
+ // - commit ad7ecfa618ec3a65db8405d9f1125059fe4a6a15
+ // - date of access: 2022-03-07
+ // - see: https://searchfox.org/mozilla-central/rev/ad7ecfa618ec3a65db8405d9f1125059fe4a6a15/caps/OriginAttributes.cpp
+ //
+ // Order of arguments is changed to support invoking function without
+ // providing `port` argument. Original C++ code implements an overload for
+ // this purpose.
+ combineTopLevelInfoParts(useSite, scheme, host, port = '') {
+ if (!useSite) { return host; }
+ const parts = [scheme, host];
+ if (port !== '') { parts.push(port.toString()); }
+ const combinedParts = parts.join(',');
+ return `(${combinedParts})`;
+ }
+
+ // Here follows translation of C++ method `PopulateTopLevelInfoFromURI`
+ // defined in `caps/OriginAttributes.cpp` file in Firefox sources:
+ // - commit ad7ecfa618ec3a65db8405d9f1125059fe4a6a15
+ // - date of access: 2022-03-07
+ // - see: https://searchfox.org/mozilla-central/rev/ad7ecfa618ec3a65db8405d9f1125059fe4a6a15/caps/OriginAttributes.cpp
+ //
+ // (NOTE) Specific requirements to parameters of JavaScript version:
+ // - `url` - `hostname` part of URL must be canonicalized:
+ // * domain name must be lowercase, punycode, and only
+ // /[a-z0-9.-]+/ (requirement of publicsuffix.js library)
+ // * but IPv4 and IPv6 addresses, of course, can contain any
+ // valid (for IP addresses) characters in any case
+ //
+ // Returns either string with top-level info or `undefined`.
+ extractTopLevelInfo(url, useSite) {
+ // PopulateTopLevelInfoFromURI C++ method arguments:
+ //
+ // - `aIsTopLevelDocument`, `aIsFirstPartyEnabled`, `aForced`:
+ // 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.
+ //
+ // - `aOriginAttributes`, `OriginAttributes::*aTarget`:
+ // 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 returned value. So
+ // `aOriginAttributes` and `OriginAttributes::*aTarget` should be
+ // ignored and using `topLevelInfo` should be interpreted as returning
+ // result of `PopulateTopLevelInfoFromURI`.
+ //
+ // - `aURI`:
+ // string with an URL to compute topLevelInfo for
+ // (the same as `url` passed to this JavaScript function)
+ //
+ // - `aUseSite`:
+ // boolean to select whether `topLevelInfo` should include scheme and
+ // port
+ // (the same as `useSite` passed to this JavaScript function)
+
+ // PopulateTopLevelInfoFromURI: lines 58-69 are ignored.
+ //
+ // These lines contain:
+ // - irrelevant declaration of variable;
+ // - irrelevant checking of early return conditions.
+
+ const parsedUrl = new URL(url);
+ const urlScheme = parsedUrl.protocol.slice(0, -1); // Remove trailing ':'.
+
+ if (urlScheme === 'about') {
+ return this.combineTopLevelInfoParts(useSite, urlScheme,
+ // Expanded C++ macro `ABOUT_URI_FIRST_PARTY_DOMAIN`.
+ // For macro definition see:
+ // https://searchfox.org/mozilla-central/rev/cdab8eb3407f8fe216d5357c09aa5bfa479651f2/netwerk/base/nsNetUtil.h#643
+ 'about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla'
+ );
+ }
+
+ if (urlScheme === 'moz-extension') { return /* undefined */; }
+
+ // PopulateTopLevelInfoFromURI: lines 88-94.
+ //
+ // These lines get topLevelInfo when `aURL` is a Blob URL (an URL,
+ // created by JavaScript function `URL.createObjectURL()`).
+ //
+ // According to
+ // https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
+ // Blob URL lifetime is tied to the `document` in the window on which it
+ // was created.
+ //
+ // I believe, it means that such URL can not be an URL in browser location
+ // bar. Therefore, absence of its' handling is not a deficience.
+ //
+ // Try to support it anyway.
+ // It has format `blob:https://an.example.com/<uuid>`.
+
+ // PopulateTopLevelInfoFromURI: lines 96-97.
+ //
+ // Functions provided by `nsIEffectiveTLDService` are documented/defined
+ // in:
+ // * https://searchfox.org/mozilla-central/rev/980b50947e2a855c92f2df74209dadad3ee4d119/netwerk/dns/nsIEffectiveTLDService.idl
+ // * https://searchfox.org/mozilla-central/rev/cdab8eb3407f8fe216d5357c09aa5bfa479651f2/netwerk/dns/nsEffectiveTLDService.cpp
+ //
+ // See:
+ // * https://wiki.mozilla.org/Gecko:Effective_TLD_Service
+ // * https://web.archive.org/web/20191001223947/https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIEffectiveTLDService
+ // * comments in https://bugzilla.mozilla.org/show_bug.cgi?id=368989
+ // * https://bugzilla.mozilla.org/show_bug.cgi?id=1621168
+
+ // PopulateTopLevelInfoFromURI: lines 98-111 are skipped (for a moment).
+ //
+ // Getting base domain is performed after checking if host is IP address.
+
+ const {hostname: host, port, protocol} = (
+ (urlScheme === 'blob')
+ ? new URL(url.slice(urlScheme.length + 1))
+ : parsedUrl
+ );
+ const scheme = protocol.slice(0, -1); // Remove trailing ':'.
+
+ if (ipV4Regex.test(host)) {
+ return this.combineTopLevelInfoParts(useSite, scheme, host, port);
+ }
+
+ if (ipV6Regex.test(host)) {
+ return this.combineTopLevelInfoParts(useSite, scheme, `[${host}]`, port);
+ }
+ const maybeIpV6 = host.match(/^\[(.+)\]$/u);
+ if ((maybeIpV6 !== null) && ipV6Regex.test(maybeIpV6[1])) {
+ return this.combineTopLevelInfoParts(useSite, scheme, host, port);
+ }
+
+ // Here follows implementation of relevant parts of `GetBaseDomain` (and
+ // `GetBaseDomainInternal`) provided by `nsEffectiveTLDService`.
+ const trailingHostDot = host.endsWith('.') ? '.' : '';
+ const hostWithoutEndDot = (host.endsWith('.') ? host.slice(0, -1) : host);
+ if ((hostWithoutEndDot === '') || hostWithoutEndDot.endsWith('.')) {
+ // Returns NS_ERROR_INVALID_ARG in `GetBaseDomainInternal`.
+ return /* undefined */;
+ }
+ if (/(?:^\.|\.\.)/u.test(hostWithoutEndDot)) {
+ // `host` has either leading dot, or two (or more) consecutive dots.
+ // Returns NS_ERROR_INVALID_ARG in `GetBaseDomainInternal`.
+ return /* undefined */;
+ }
+ // Argument of `getPublicSuffix()` and `getDomain()` must be
+ // "canonicalized": it must be lowercase, punycode, only /[a-z0-9.-]+/
+ // and without trailing dot. (requirement of publicsuffix.js library)
+ const publicSuffix = publicSuffixList.dat.getPublicSuffix(hostWithoutEndDot);
+ const isInsuffcientDomainLevels = (publicSuffix === hostWithoutEndDot);
+ const baseDomain = publicSuffixList.dat.getDomain(hostWithoutEndDot);
+
+ // PopulateTopLevelInfoFromURI: lines 101-106.
+
+ if (baseDomain !== '') {
+ // Concatenating with trailing dot is done in `GetBaseDomainInternal`.
+ const domain = `${baseDomain}${trailingHostDot}`;
+ return this.combineTopLevelInfoParts(useSite, scheme, domain);
+ }
+
+ // PopulateTopLevelInfoFromURI: lines 140 to the end of function.
+
+ if (useSite) {
+ return this.combineTopLevelInfoParts(useSite, scheme, host, port);
+ }
+
+ if (isInsufficientDomainLevels && (publicSuffix !== '')) {
+ // Concatenating with trailing dot is done in `GetBaseDomainInternal`.
+ const domain = `${publicSuffix}${trailingHostDot}`;
+ return this.combineTopLevelInfoParts(useSite, scheme, domain, port);
+ }
+
+ return /* undefined */;
+ }
+
+ // Guess, whether `privacy.firstparty.isolate` is enabled in `about:config`.
+ async isPrivacyFirstpartyIsolateTrue(cookiesFilter) {
+ try {
+ // Exclude firstPartyDomain from cookiesFilter.
+ const {firstPartyDomain, ...incompleteCookiesFilter} = cookiesFilter;
+ await browser.cookies.getAll(incompleteCookiesFilter);
+ } catch (e) {
+ if (e.message === "First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set.") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Compute `firstPartyDomain` from `tabUrl`, making a guess whether
+ // `privacy.firstparty.isolate.use_site` is enabled in `about:config`.
+ //
+ // Note: the `firstPartyDomain` value is not guessed, but computed according
+ // to method re-implemented from Firefox C++ code. Only concrete method of
+ // its' computing is guessed.
+ async guessFirstPartyDomain(cookiesFilter, tabUrl) {
+ const schemeful = this.extractTopLevelInfo(tabUrl, /*useSite*/ true);
+ const schemefulFilter = {...cookiesFilter, firstPartyDomain: schemeful}
+ const cookies = await browser.cookies.getAll(schemefulFilter);
+ // Make a guess between schemeful and schemless `firstPartyDomain`.
+ return (
+ (cookies.length > 0)
+ ? schemeful
+ : this.extractTopLevelInfo(tabUrl, /*useSite*/ false)
+ );
+ }
+
+ // Return result of `browser.cookies.getAll(cookiesFilter)`, heuristically
+ // finding required `firstPartyDomain` or `partitionKey` values to include
+ // in `cookiesFilter`.
+ async getCookies(cookiesFilter, tabUrl) {
+ // Definitions:
+ // * "top-level info" is a string computed from URL's hostname according
+ // to rules defined in Firefox C++ code (practically, in most cases
+ // it's a "base domain": part of hostname up to, and including, first
+ // sub-domain of "public suffix"/"effective top-level domain"/"eTLD")
+ // - https://publicsuffix.org/
+ // - https://en.wikipedia.org/wiki/Public_Suffix_List
+ // - https://wiki.mozilla.org/Public_Suffix_List
+ // * "first-party" means all resources with "top-level info" identical to
+ // "top-level info" of an URL in Firefox location bar
+ // * "third-party" means all resources whose are not "first-party"
+ //
+ // Cookies included by browser in HTTP request are selected from cookie
+ // jar by "key". In standard browser behavior, this key is computed from
+ // requested URL and compared to combination of values provided by cookie
+ // itself (in "Domain", "SameSite, "Secure" cookie attributes etc.).
+ //
+ // Standard behavior opens a way for a third-party to track, store and
+ // analyze browsing history of concrete user across multiple independent
+ // first-parties, building user profile and defying user privacy.
+ //
+ // https://en.wikipedia.org/wiki/HTTP_cookie#Third-party_cookie
+ // https://en.wikipedia.org/wiki/HTTP_cookie#Privacy_and_third-party_cookies
+ // https://support.mozilla.org/en-US/kb/third-party-trackers
+ //
+ // To prevent these consequences, Firefox could be put in mode, when it
+ // additionaly computes first-party top-level info to store in cookie jar
+ // along with received cookie. Mechanism, implemented in Firefox for this
+ // purpose, was named "origin attributes" and first-party top-level info
+ // is one of "origin attributes".
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=565965
+ // "Key cookies on setting domain * toplevel load domain"
+ // - https://wiki.mozilla.org/Thirdparty
+ // (linked from comment 16 of that issue:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=565965#c16)
+ //
+ // Origin attributes are used by Firefox for "isolating" cookies by using
+ // these attributes along with requested URL for computing "key" to select
+ // cookies (from cookie jar) to include in HTTP request.
+ //
+ // https://support.mozilla.org/en-US/kb/third-party-cookies-firefox-tracking-protection
+ //
+ // Firefox has two mechanisms that use origin attributes to provide
+ // cookie isolation:
+ // * First-party isolation ("FPI")
+ // * Total cookie protection ("dynamic first-party isolation", "dFPI",
+ // part of "state paritioning" mechanism).
+ //
+ // Overall view of these features is summarized at:
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1669716#c10
+ // > 3. 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.
+ // - https://github.com/mozilla/multi-account-containers/issues/1974#issuecomment-785243612
+ // "Total Cookie Protection comparison"
+ // > As I understand it, Total Cookie Protect is a product name and
+ // evolution of dynamic first-party isolation. In dFPI Firefox
+ // isolates cookies to the first-party domain, with an exception
+ // (the "dynamic" part) for certain user-initiated resource requests
+ // like those used in Single Sign-On implementations.
+ // - https://github.com/arkenfox/user.js/issues/1051#issuecomment-809645683
+ // "move from FPI to dFPI"
+ // - https://github.com/arkenfox/user.js/issues/8
+ // "meta: tor uplift: privacy.firstparty.isolate"
+ // - https://github.com/arkenfox/user.js/issues/395
+ // "Temporary Containers vs First-Party Isolation"
+ //
+ // FPI and dFPI:
+ // * are mutually exclusive since Firefox 78 (FPI has a precedence, when
+ // both are enabled):
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1631676
+ // "Disable dfpi when privacy.firstparty.isolate=true"
+ // * use separate origin attributes (firstPartyDomain and partitionKey)
+ // since Firefox 79:
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1640135
+ // "Consider using separate attribute to store first-party domain for dFPI"
+ //
+ // First-party isolation feature:
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1299996
+ // "[META] Support Tor first-party isolation"
+ // - introduced in Firefox 51 (released on January 24, 2017)
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1260931
+ // "Add 1st party isolation pref and OriginAttribute."
+ // - with support in `cookies` WebExtension API added (by introducing
+ // `firstPartyDomain` key in parameters/return values) in Firefox 59
+ // - https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/59#webextensions
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1381197
+ // "browser.cookies fails to get/remove cookies by domain/url when privacy.firstparty.isolate = true"
+ // + feature design was proposed in comment 6 of that issue
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1381197#c6
+ // and discussed further in following comments
+ // - with preference for computing schemful top-level info added
+ // in Firefox 78
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1637516#c15
+ // "Consider changing the key of StoragePrincipal from registrable domain (eTLD+1) to site"
+ //
+ // Total cookie protection feature:
+ // - introduced in Firefox 86 (released on February 23, 2021)
+ // - https://www.mozilla.org/en-US/firefox/86.0/releasenotes/
+ // - https://blog.mozilla.org/security/2021/02/23/total-cookie-protection/
+ // - https://hacks.mozilla.org/2021/02/introducing-state-partitioning/
+ // - https://blog.mozilla.org/security/2021/01/26/supercookie-protections/
+ // - https://support.mozilla.org/en-US/kb/third-party-cookies-firefox-tracking-protection
+ // - https://reddit.com/r/privacy/comments/lqkouc/firefox_announces_total_cookie_protection/
+ // - with support in `cookies` WebExtension API added (by introducing
+ // `partitionKey` key in parameters/return values) in Firefox 94
+ // - https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/94#changes_for_add-on_developers
+ // - https://bugzilla.mozilla.org/show_bug.cgi?id=1669716
+ // "cookies extension API unaware of dFPI; firstPartyDomain does not work with dynamic First-Party Isolation"
+ //
+ // According to https://bugzilla.mozilla.org/show_bug.cgi?id=1669716#c10:
+ // - FPI could be enabled only by setting `privacy.firstparty.isolate`
+ // to `true` in `about:config` (it's `false` by default). Additionaly,
+ // `privacy.firstparty.isolate.use_site` controls schemefullness of
+ // first-party top-level info as it used by enabled FPI
+ // (`true`: schemeful, `false`: schemeless)
+ // - in stock Firefox dFPI feature is enabled by default
+ // - in all windows (for Firefox Nightly)
+ // - in Private browsing window (for other release channels)
+ // - dFPI is also enabled when "Enhanced Tracking Protection" at
+ // `about:preferences#privacy` is set to "Strict"
+ // - in fact, dFPI is enabled by value `5` of two independent settings
+ // at `about:config`:
+ // * network.cookie.cookieBehavior (for enabling in normal windows)
+ // * network.cookie.cookieBehavior.pbmode (for enabling in Private
+ // browsing windows)
+ //
+ // There are no clearly documented guidelines and direct answers about how
+ // exactly extension should get values of `firstPartyDomain` and
+ // `partitionKey` if it wants to keep first-party isolation guarantees.
+
+ // Check for `privacy.firstparty.isolate`.
+ if (await this.isPrivacyFirstpartyIsolateTrue(cookiesFilter)) {
+ const firstPartyDomain = await this.guessFirstPartyDomain(cookiesFilter, tabUrl);
+ if (firstPartyDomain === undefined) { return []; }
+ const isolatedCookiesFilter = {...cookiesFilter, firstPartyDomain};
+ const fpiCookies = await browser.cookies.getAll(isolatedCookiesFilter);
+ return fpiCookies;
+ }
+
+ // Check for Total cookie protection.
+ // Try to get cookies from partitioned storage. If there is any, dFPI is
+ // probably enabled.
+ const partitionKey = {topLevelSite: tabUrl};
+ const partitionedCookiesFilter = {...cookiesFilter, partitionKey};
+ const dfpiCookies = await browser.cookies.getAll(partitionedCookiesFilter);
+ if (dfpiCookies.length > 0) { return dfpiCookies; }
+
+ // FPI is not enabled, and, probably, dFPI is not enabled too.
+ const cookies = await browser.cookies.getAll(cookiesFilter);
+ return cookies;
+ }
+
+ async addCookie(url, headers, storeId, tabUrl) {
// add contexual cookies, only in container/incognito
- const cookies = await browser.cookies.getAll({url, storeId});
+ const cookies = await this.getCookies({url, storeId}, tabUrl);
const str = cookies && cookies.map(item => `${item.name}=${item.value}`).join('; ');
str && (headers['FM-Contextual-Cookie'] = str);
}
- async fetch(e, storeId) {
+ async fetch(e, storeId, tabUrl) {
if (e.init.credentials !== 'omit' && storeId) { // not anonymous AND in container/incognito
e.init.credentials = 'omit';
- await this.addCookie(e.url, e.init.headers, storeId);
+ await this.addCookie(e.url, e.init.headers, storeId, tabUrl);
}
Object.keys(e.init.headers)[0] || delete e.init.headers; // clean up

Definitions

  • top-level info: a string computed from URL's hostname according to rules defined in Firefox C++ code (practically, in most cases it's a base domain: part of hostname up to, and including, first sub-domain of public suffix AKA effective top-level domain AKA eTLD)

  • first-party: any resource with top-level info identical to the top-level info of an URL in Firefox location bar

  • third-party: any resource which is not first-party

Third-party cookies, third-party trackers and privacy considerations

Cookies included by browser in HTTP request are selected from cookie jar by key. In standard browser behavior, this key is computed from requested URL and compared to combination of values provided by cookie itself (in Domain, SameSite, Secure cookie attributes etc.).

Standard behavior opens a way for a third-party to track, store and analyze browsing history of concrete user across multiple independent first-parties, building user profile and defying user privacy.

Firefox can isolate third-party cookies and provide tracking protection

To prevent third-parties from snooping on user, Firefox could be put in mode, when it additionaly computes first-party top-level info to store in cookie jar along with received cookie. Mechanism, implemented in Firefox for this purpose, was named origin attributes and first-party top-level info is one of origin attributes.

Origin attributes are used by Firefox for "isolating" cookies by using these attributes along with requested URL for computing key to select cookies (from cookie jar) to include in HTTP request.

support.mozilla.org / Third-party cookies and Firefox tracking protection

FPI and dFPI

Firefox has two mechanisms that use origin attributes to provide cookie isolation:

  • first-party isolation ("FPI")
  • Total cookie protection ("dynamic first-party isolation", "dFPI", part of "state paritioning" mechanism).
Overall view of these features is summarized at:
  • comment 10 of bugzilla #1669716 (cookies extension API unaware of dFPI; firstPartyDomain does not work with dynamic First-Party Isolation)

    3. 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.

  • comment of GitHub / mozilla/multi-account-containers > issue #1974 (Total Cookie Protection comparison)

    As I understand it, Total Cookie Protect is a product name and evolution of dynamic first-party isolation. In dFPI Firefox isolates cookies to the first-party domain, with an exception (the "dynamic" part) for certain user-initiated resource requests like those used in Single Sign-On implementations.

  • comment of GitHub / arkenfox/user.js > issue #1051 (move from FPI to dFPI)

  • GitHub / arkenfox/user.js > issue #8 (meta: tor uplift: privacy.firstparty.isolate)

  • GitHub / arkenfox/user.js > issue #395 (Temporary Containers vs First-Party Isolation)

Relations between FPI and dFPI

FPI and dFPI:

  • are mutually exclusive since Firefox 78 (FPI has a precedence, when both are enabled)
    bugzilla #1631676 (Disable dfpi when privacy.firstparty.isolate=true)

  • use separate origin attributes (firstPartyDomain and partitionKey) since Firefox 79
    bugzilla #1640135 (Consider using separate attribute to store first-party domain for dFPI)

First-party isolation feature

  • bugzilla #1299996 ([META] Support Tor first-party isolation)

  • introduced in Firefox 51 (released on January 24, 2017)
    bugzilla #1260931 (Add 1st party isolation pref and OriginAttribute.)

  • with support in cookies WebExtension API added (by introducing firstPartyDomain key in parameters/return values) in Firefox 59:

  • with preference for computing schemful top-level info added in Firefox 78
    comment 15 of bugzilla #1637516 (Consider changing the key of StoragePrincipal from registrable domain (eTLD+1) to site)

Total cookie protection feature

When FPI/dFPI are enabled?

According to comment 10 of bugzilla #1669716:

  • FPI could be enabled only by setting privacy.firstparty.isolate to true in about:config (it's false by default). Additionaly, privacy.firstparty.isolate.use_site controls schemefullness of first-party top-level info as it used by enabled FPI (true: schemeful, false: schemeless)
  • in stock Firefox dFPI feature is enabled by default:

    • in all windows (for Firefox Nightly)
    • in Private browsing window (for other release channels)
  • dFPI is also enabled when "Enhanced Tracking Protection" at about:preferences#privacy is set to "Strict"

  • in fact, dFPI is enabled by value 5 of two independent settings at about:config:

    • network.cookie.cookieBehavior (for enabling in normal windows)
    • network.cookie.cookieBehavior.pbmode (for enabling in Private browsing windows)

There are no clearly documented guidelines and direct answers about how exactly extension should get values of firstPartyDomain and partitionKey if it wants to keep first-party isolation guarantees.

import {ipV4Regex, ipV6Regex, publicSuffixList} from './vendor/index.js';
// Here follows translation of C++ method `MakeTopLevelInfo`
// defined in `caps/OriginAttributes.cpp` file in Firefox sources:
// - commit ad7ecfa618ec3a65db8405d9f1125059fe4a6a15
// - date of access: 2022-03-07
// - see: https://searchfox.org/mozilla-central/rev/ad7ecfa618ec3a65db8405d9f1125059fe4a6a15/caps/OriginAttributes.cpp
//
// Order of arguments is changed to support invoking function without
// providing `port` argument. Original C++ code implements an overload for
// this purpose.
combineTopLevelInfoParts(useSite, scheme, host, port = '') {
if (!useSite) { return host; }
const parts = [scheme, host];
if (port !== '') { parts.push(port.toString()); }
const combinedParts = parts.join(',');
return `(${combinedParts})`;
}
// Here follows translation of C++ method `PopulateTopLevelInfoFromURI`
// defined in `caps/OriginAttributes.cpp` file in Firefox sources:
// - commit ad7ecfa618ec3a65db8405d9f1125059fe4a6a15
// - date of access: 2022-03-07
// - see: https://searchfox.org/mozilla-central/rev/ad7ecfa618ec3a65db8405d9f1125059fe4a6a15/caps/OriginAttributes.cpp
//
// (NOTE) Specific requirements to parameters of JavaScript version:
// - `url` - `hostname` part of URL must be canonicalized:
// * domain name must be lowercase, punycode, and only
// /[a-z0-9.-]+/ (requirement of publicsuffix.js library)
// * but IPv4 and IPv6 addresses, of course, can contain any
// valid (for IP addresses) characters in any case
//
// Returns either string with top-level info or `undefined`.
extractTopLevelInfo(url, useSite) {
// PopulateTopLevelInfoFromURI C++ method arguments:
//
// - `aIsTopLevelDocument`, `aIsFirstPartyEnabled`, `aForced`:
// 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.
//
// - `aOriginAttributes`, `OriginAttributes::*aTarget`:
// 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 returned value. So
// `aOriginAttributes` and `OriginAttributes::*aTarget` should be
// ignored and using `topLevelInfo` should be interpreted as returning
// result of `PopulateTopLevelInfoFromURI`.
//
// - `aURI`:
// string with an URL to compute topLevelInfo for
// (the same as `url` passed to this JavaScript function)
//
// - `aUseSite`:
// boolean to select whether `topLevelInfo` should include scheme and
// port
// (the same as `useSite` passed to this JavaScript function)
// PopulateTopLevelInfoFromURI: lines 58-69 are ignored.
//
// These lines contain:
// - irrelevant declaration of variable;
// - irrelevant checking of early return conditions.
const parsedUrl = new URL(url);
const urlScheme = parsedUrl.protocol.slice(0, -1); // Remove trailing ':'.
if (urlScheme === 'about') {
return combineTopLevelInfoParts(useSite, urlScheme,
// Expanded C++ macro `ABOUT_URI_FIRST_PARTY_DOMAIN`.
// For macro definition see:
// https://searchfox.org/mozilla-central/rev/cdab8eb3407f8fe216d5357c09aa5bfa479651f2/netwerk/base/nsNetUtil.h#643
'about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla'
);
}
if (urlScheme === 'moz-extension') { return /* undefined */; }
// PopulateTopLevelInfoFromURI: lines 88-94.
//
// These lines get topLevelInfo when `aURL` is a Blob URL (an URL,
// created by JavaScript function `URL.createObjectURL()`).
//
// According to
// https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
// Blob URL lifetime is tied to the `document` in the window on which it
// was created.
//
// I believe, it means that such URL can not be an URL in browser location
// bar. Therefore, absence of its' handling is not a deficience.
//
// Try to support it anyway.
// It has format `blob:https://an.example.com/<uuid>`.
// PopulateTopLevelInfoFromURI: lines 96-97.
//
// Functions provided by `nsIEffectiveTLDService` are documented/defined
// in:
// * https://searchfox.org/mozilla-central/rev/980b50947e2a855c92f2df74209dadad3ee4d119/netwerk/dns/nsIEffectiveTLDService.idl
// * https://searchfox.org/mozilla-central/rev/cdab8eb3407f8fe216d5357c09aa5bfa479651f2/netwerk/dns/nsEffectiveTLDService.cpp
//
// See:
// * https://wiki.mozilla.org/Gecko:Effective_TLD_Service
// * https://web.archive.org/web/20191001223947/https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIEffectiveTLDService
// * comments in https://bugzilla.mozilla.org/show_bug.cgi?id=368989
// * https://bugzilla.mozilla.org/show_bug.cgi?id=1621168
// PopulateTopLevelInfoFromURI: lines 98-111 are skipped (for a moment).
//
// Getting base domain is performed after checking if host is IP address.
const {hostname: host, port, protocol} = (
(urlScheme === 'blob')
? new URL(url.slice(urlScheme.length + 1))
: parsedUrl
);
const scheme = protocol.slice(0, -1); // Remove trailing ':'.
if (ipV4Regex.test(host)) {
return combineTopLevelInfoParts(useSite, scheme, host, port);
}
if (ipV6Regex.test(host)) {
return combineTopLevelInfoParts(useSite, scheme, `[${host}]`, port);
}
const maybeIpV6 = host.match(/^\[(.+)\]$/u);
if ((maybeIpV6 !== null) && ipV6Regex.test(maybeIpV6[1])) {
return combineTopLevelInfoParts(useSite, scheme, host, port);
}
// Here follows implementation of relevant parts of `GetBaseDomain` (and
// `GetBaseDomainInternal`) provided by `nsEffectiveTLDService`.
const trailingHostDot = host.endsWith('.') ? '.' : '';
const hostWithoutEndDot = (host.endsWith('.') ? host.slice(0, -1) : host);
if ((hostWithoutEndDot === '') || hostWithoutEndDot.endsWith('.')) {
// Returns NS_ERROR_INVALID_ARG in `GetBaseDomainInternal`.
return /* undefined */;
}
if (/(?:^\.|\.\.)/u.test(hostWithoutEndDot)) {
// `host` has either leading dot, or two (or more) consecutive dots.
// Returns NS_ERROR_INVALID_ARG in `GetBaseDomainInternal`.
return /* undefined */;
}
// Argument of `getPublicSuffix()` and `getDomain()` must be
// "canonicalized": it must be lowercase, punycode, only /[a-z0-9.-]+/
// and without trailing dot. (requirement of publicsuffix.js library)
const publicSuffix = publicSuffixList.dat.getPublicSuffix(hostWithoutEndDot);
const isInsuffcientDomainLevels = (publicSuffix === hostWithoutEndDot);
const baseDomain = publicSuffixList.dat.getDomain(hostWithoutEndDot);
// PopulateTopLevelInfoFromURI: lines 101-106.
if (baseDomain !== '') {
// Concatenating with trailing dot is done in `GetBaseDomainInternal`.
const domain = `${baseDomain}${trailingHostDot}`;
return combineTopLevelInfoParts(useSite, scheme, domain);
}
// PopulateTopLevelInfoFromURI: lines 140 to the end of function.
if (useSite) {
return combineTopLevelInfoParts(useSite, scheme, host, port);
}
if (isInsufficientDomainLevels && (publicSuffix !== '')) {
// Concatenating with trailing dot is done in `GetBaseDomainInternal`.
const domain = `${publicSuffix}${trailingHostDot}`;
return combineTopLevelInfoParts(useSite, scheme, domain, port);
}
return /* undefined */;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment