Backed out changeset b8b6e3f85b20 (bug 1873418) for causing failures at test_getPartitionKeyFromURL.js. CLOSED TREE

This commit is contained in:
Butkovits Atila 2024-09-26 02:03:50 +03:00
parent a4fab2a464
commit 18cff65ed8
7 changed files with 123 additions and 385 deletions

View File

@ -7,7 +7,6 @@
#include "ChromeUtils.h"
#include "JSOracleParent.h"
#include "ThirdPartyUtil.h"
#include "js/CallAndConstruct.h" // JS::Call
#include "js/ColumnNumber.h" // JS::TaggedColumnNumberOneOrigin, JS::ColumnNumberOneOrigin
#include "js/CharacterEncoding.h"
@ -1311,64 +1310,27 @@ void ChromeUtils::GetBaseDomainFromPartitionKey(dom::GlobalObject& aGlobal,
/* static */
void ChromeUtils::GetPartitionKeyFromURL(dom::GlobalObject& aGlobal,
const nsAString& aTopLevelUrl,
const nsAString& aSubresourceUrl,
const Optional<bool>& aForeignContext,
const nsAString& aURL,
nsAString& aPartitionKey,
ErrorResult& aRv) {
nsCOMPtr<nsIURI> topLevelURI;
nsresult rv = NS_NewURI(getter_AddRefs(topLevelURI), aTopLevelUrl);
if (NS_SUCCEEDED(rv) && topLevelURI->SchemeIs("chrome")) {
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
if (NS_SUCCEEDED(rv) && uri->SchemeIs("chrome")) {
rv = NS_ERROR_FAILURE;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
aPartitionKey.Truncate();
aRv.Throw(rv);
return;
}
bool foreignResource;
bool fallback = false;
if (aSubresourceUrl.Length() > 0) {
nsCOMPtr<nsIURI> resourceURI;
rv = NS_NewURI(getter_AddRefs(resourceURI), aSubresourceUrl);
if (NS_WARN_IF(NS_FAILED(rv))) {
aPartitionKey.Truncate();
aRv.Throw(rv);
return;
}
ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
if (!thirdPartyUtil) {
aPartitionKey.Truncate();
aRv.Throw(NS_ERROR_SERVICE_NOT_AVAILABLE);
return;
}
rv = thirdPartyUtil->IsThirdPartyURI(topLevelURI, resourceURI,
&foreignResource);
if (NS_FAILED(rv)) {
// we fallback to assuming the resource is foreign if there is an error
foreignResource = true;
fallback = true;
}
} else {
// Assume we have a foreign resource if the resource was not provided
foreignResource = true;
fallback = true;
}
if (aForeignContext.WasPassed() && !aForeignContext.Value() &&
foreignResource && !fallback) {
aPartitionKey.Truncate();
aRv.Throw(nsresult::NS_ERROR_INVALID_ARG);
return;
}
bool foreignByAncestorContext = aForeignContext.WasPassed() &&
aForeignContext.Value() && !foreignResource;
mozilla::OriginAttributes attrs;
attrs.SetPartitionKey(topLevelURI, foreignByAncestorContext);
// For now, uses assume the partition key is cross-site.
// We will need to not make this assumption to allow access
// to same-site partitioned cookies in the cookie extension API.
attrs.SetPartitionKey(uri, false);
aPartitionKey = attrs.mPartitionKey;
}

View File

@ -139,9 +139,7 @@ class ChromeUtils {
ErrorResult& aRv);
static void GetPartitionKeyFromURL(dom::GlobalObject& aGlobal,
const nsAString& aTopLevelUrl,
const nsAString& aSubresourceUrl,
const Optional<bool>& aForeignContext,
const nsAString& aURL,
nsAString& aPartitionKey,
ErrorResult& aRv);

View File

@ -454,20 +454,16 @@ partial namespace ChromeUtils {
getBaseDomainFromPartitionKey(DOMString partitionKey);
/**
* Returns the partitionKey for a given subresourceURL given its top-level URL
* and whether or not it is in a foreign context.
* Returns the partitionKey for a given URL.
*
* The function will treat the topLevelURL as a first party and construct the
* partitionKey according to the scheme, site and port in the URL. It will also
* include information about the subresource and whether or not this is a foreign
* request in the partition key.
* The function will treat the URL as a first party and construct the
* partitionKey according to the scheme, site and port in the URL.
*
* Throws for invalid urls, if the Third Party Service is unavailable, or if the
* combination of inputs is impossible.
* Throws for invalid urls.
*/
[Throws]
DOMString
getPartitionKeyFromURL(DOMString topLevelUrl, DOMString subresourceUrl, optional boolean foreignContext);
getPartitionKeyFromURL(DOMString url);
/**
* Loads and compiles the script at the given URL and returns an object

View File

@ -758,16 +758,12 @@ class StorageModule extends RootBiDiModule {
originAttributes.partitionKey = "";
} else {
originAttributes.partitionKey = ChromeUtils.getPartitionKeyFromURL(
partitionKey.sourceOrigin,
"",
false
partitionKey.sourceOrigin
);
}
} else {
originAttributes.partitionKey = ChromeUtils.getPartitionKeyFromURL(
partitionKey.sourceOrigin,
"",
false
partitionKey.sourceOrigin
);
}
}

View File

@ -30,60 +30,30 @@ const dropBracketIfIPv6 = host =>
? host.slice(1, -1)
: host;
const isSubdomain = (otherDomain, baseDomain) => {
return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
};
// Converts the partitionKey format of the extension API (i.e. PartitionKey) to
// a valid format for the "partitionKey" member of OriginAttributes.
function fromExtPartitionKey(extPartitionKey, cookieUrl) {
function fromExtPartitionKey(extPartitionKey) {
if (!extPartitionKey) {
// Unpartitioned by default.
return "";
}
const { topLevelSite, hasCrossSiteAncestor } = extPartitionKey;
const { topLevelSite } = extPartitionKey;
// TODO: Expand API to force the generation of a partitionKey that differs
// from the default that's specified by privacy.dynamic_firstparty.use_site.
if (topLevelSite) {
// If topLevelSite is set and a non-empty string (a site in a URL format).
try {
// This is subtle! We define the ancestor bit in our code in a different
// way than the extension API, but they are isomorphic.
// If we have cookieUrl (which is guaranteed to be the case in get, set,
// and remove) this will return the topLevelSite parsed partition key,
// and include the foreign ancestor bit iff the details.url is
// same-site and a truthy value was passed in the hasCrossSiteAncestor
// property. If we don't have cookieUrl, we handle the difference in
// ancestor bit definition by returning a OA pattern that matches both
// values and filtering them later on in matches.
if (cookieUrl == null) {
let topLevelSiteURI = Services.io.newURI(topLevelSite);
let topLevelSiteFilter = Services.eTLD.getSite(topLevelSiteURI);
if (topLevelSiteURI.port != -1) {
topLevelSiteFilter += `:${topLevelSiteURI.port}`;
}
return topLevelSiteFilter;
}
return ChromeUtils.getPartitionKeyFromURL(
topLevelSite,
cookieUrl,
hasCrossSiteAncestor ?? undefined
);
return ChromeUtils.getPartitionKeyFromURL(topLevelSite);
} catch (e) {
throw new ExtensionError("Invalid value for 'partitionKey' attribute");
}
} else if (topLevelSite == null && hasCrossSiteAncestor != null) {
// This is an invalid combination of parameters.
throw new ExtensionError("Invalid value for 'partitionKey' attribute");
}
// Unpartitioned.
return "";
}
// Converts an internal partitionKey (format used by OriginAttributes) to the
// string value as exposed through the extension API.
function getExtPartitionKey(cookie) {
let partitionKey = cookie.originAttributes.partitionKey;
function toExtPartitionKey(partitionKey) {
if (!partitionKey) {
// Canonical representation of an empty partitionKey is null.
// In theory {topLevelSite: ""} also works, but alas.
@ -95,34 +65,15 @@ function getExtPartitionKey(cookie) {
// pref, which is not necessarily the case for cookies before the pref flip.
if (!partitionKey.startsWith("(")) {
// A partitionKey generated with privacy.dynamic_firstparty.use_site=false.
let hasCrossSiteAncestor = !isSubdomain(cookie.host, partitionKey);
return { topLevelSite: `https://${partitionKey}`, hasCrossSiteAncestor };
return { topLevelSite: `https://${partitionKey}` };
}
// partitionKey starts with "(" and ends with ")".
let [scheme, domain, opt1, opt2] = partitionKey.slice(1, -1).split(",");
// foreignByAncestorContext logic from OriginAttributes::ParsePartitionKey.
let fbac = false;
let port;
if (opt2) {
// opt2 is "f" or undefined.
port = opt1;
fbac = true;
} else if (opt1 == "f") {
fbac = true;
} else if (opt1) {
port = opt1;
}
// Construct the topLevelSite part of the partitionKey
let [scheme, domain, port] = partitionKey.slice(1, -1).split(",");
let topLevelSite = `${scheme}://${domain}`;
if (port) {
topLevelSite += `:${port}`;
}
// Construct the hasCrossSiteAncestor bit as well.
// This is isomorphic, but not identical to how we partition.
let hasCrossSiteAncestor = fbac || !isSubdomain(cookie.host, domain);
return { topLevelSite, hasCrossSiteAncestor };
return { topLevelSite };
}
const convertCookie = ({ cookie, isPrivate }) => {
@ -137,7 +88,7 @@ const convertCookie = ({ cookie, isPrivate }) => {
sameSite: SAME_SITE_STATUSES[cookie.sameSite],
session: cookie.isSession,
firstPartyDomain: cookie.originAttributes.firstPartyDomain || "",
partitionKey: getExtPartitionKey(cookie),
partitionKey: toExtPartitionKey(cookie.originAttributes.partitionKey),
};
if (!cookie.isSession) {
@ -157,6 +108,10 @@ const convertCookie = ({ cookie, isPrivate }) => {
return result;
};
const isSubdomain = (otherDomain, baseDomain) => {
return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
};
// Checks that the given extension has permission to set the given cookie for
// the given URI.
const checkSetCookiePermissions = (extension, uri, cookie) => {
@ -285,7 +240,7 @@ const oaFromDetails = (details, context, allowPattern) => {
privateBrowsingId: 0,
// The following two keys may be deleted if allowPattern=true
firstPartyDomain: details.firstPartyDomain ?? "",
partitionKey: fromExtPartitionKey(details.partitionKey, details.url),
partitionKey: fromExtPartitionKey(details.partitionKey),
};
let isPrivate = context.incognito;
@ -315,9 +270,8 @@ const oaFromDetails = (details, context, allowPattern) => {
}
}
// If any of the originAttributes's keys are deleted, isPattern becomes true.
// If any of the originAttributes's keys are deleted, this becomes true.
let isPattern = false;
let topLevelSiteFilter;
if (allowPattern) {
// firstPartyDomain is unset / void / string.
// If unset, then we default to non-FPI cookies (or if FPI is enabled,
@ -338,23 +292,9 @@ const oaFromDetails = (details, context, allowPattern) => {
if (details.partitionKey && details.partitionKey.topLevelSite == null) {
delete originAttributes.partitionKey;
isPattern = true;
} else if (details.partitionKey?.topLevelSite && details.url == null) {
// See "This is subtle!" comment in fromExtPartitionKey.
// Matching foreignAncestorBit (hasCrossSiteAncestor) exactly
// requires a url. If url is absent, we need to filter afterwards.
topLevelSiteFilter = originAttributes.partitionKey;
delete originAttributes.partitionKey;
isPattern = true;
}
}
return {
originAttributes,
isPattern,
isPrivate,
storeId,
topLevelSiteFilter,
};
return { originAttributes, isPattern, isPrivate, storeId };
};
/**
@ -388,8 +328,7 @@ const query = function* (detailsIn, props, context, allowPattern) {
}
throw e;
}
let { originAttributes, isPattern, isPrivate, storeId, topLevelSiteFilter } =
parsedOA;
let { originAttributes, isPattern, isPrivate, storeId } = parsedOA;
if ("domain" in details) {
details.domain = details.domain.toLowerCase().replace(/^\./, "");
@ -491,29 +430,6 @@ const query = function* (detailsIn, props, context, allowPattern) {
return false;
}
// We query for more cookies than match the partitionKey parameter in cookies.getAll,
// so we must filter them down here to make sure the provided details match.
if (topLevelSiteFilter) {
let cookiePartitionKey = getExtPartitionKey(cookie);
let cookiePartitionSite = cookiePartitionKey?.topLevelSite;
// Getting here implies that we are interested in partitioned
// cookies. If there is no partition, skip the cookie.
// We also skip the cookie if it doesn't have a matching site
// componenet.
if (!cookiePartitionKey || topLevelSiteFilter !== cookiePartitionSite) {
return false;
}
if (
detailsIn.partitionKey.hasCrossSiteAncestor != null &&
detailsIn.partitionKey.hasCrossSiteAncestor !=
cookiePartitionKey.hasCrossSiteAncestor
) {
return false;
}
}
return true;
}
@ -737,7 +653,9 @@ this.cookies = class extends ExtensionAPIPersistent {
name: details.name,
storeId,
firstPartyDomain: cookie.originAttributes.firstPartyDomain,
partitionKey: getExtPartitionKey(cookie),
partitionKey: toExtPartitionKey(
cookie.originAttributes.partitionKey
),
});
}

View File

@ -33,11 +33,6 @@
"type": "string",
"optional": true,
"description": "The first-party URL of the cookie, if the cookie is in storage partitioned by the top-level site."
},
"hasCrossSiteAncestor": {
"type": "boolean",
"optional": true,
"description": "Whether or not the cookie is in a third-party context, respecting ancestor chains."
}
}
},

View File

@ -363,10 +363,7 @@ add_task(async function test_dfpi() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: {
topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}`,
hasCrossSiteAncestor: true,
},
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
},
},
];
@ -398,10 +395,7 @@ add_task(async function test_dfpi_with_ip_and_port() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: {
topLevelSite: `http://${LOCAL_IP_AND_PORT}`,
hasCrossSiteAncestor: true,
},
partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` },
},
},
];
@ -435,10 +429,7 @@ add_task(async function test_dfpi_with_nested_subdomains() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: {
topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}`,
hasCrossSiteAncestor: true,
},
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
},
},
];
@ -449,6 +440,89 @@ add_task(async function test_dfpi_with_nested_subdomains() {
);
});
add_task(async function test_dfpi_with_non_default_use_site() {
// privacy.dynamic_firstparty.use_site is a pref that can be used to toggle
// the internal representation of partitionKey. True (default) means keyed
// by site (scheme, host, port); false means keyed by host only.
const testCases = [
{
description: "first-party cookies with dFPI and use_site=false",
domain: FIRST_DOMAIN,
detailsIn: {
partitionKey: null,
},
expectedOut: {
firstPartyDomain: "",
partitionKey: null,
},
},
{
description: "third-party cookies with dFPI and use_site=false",
domain: THIRD_PARTY_DOMAIN,
detailsIn: {
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
},
expectedOut: {
firstPartyDomain: "",
// When use_site=false, the scheme is not stored, and the
// implementation just prepends "https" as a dummy scheme.
partitionKey: { topLevelSite: `https://${FIRST_DOMAIN_ETLD_PLUS_1}` },
},
},
];
await runWithPrefs(
[
// Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.
["network.cookie.cookieBehavior", 5],
["privacy.dynamic_firstparty.use_site", false],
],
() => testCookiesAPI({ testCases })
);
});
add_task(async function test_dfpi_with_ip_and_port_and_non_default_use_site() {
// privacy.dynamic_firstparty.use_site is a pref that can be used to toggle
// the internal representation of partitionKey. True (default) means keyed
// by site (scheme, host, port); false means keyed by host only.
const testCases = [
{
description: "first-party cookies for IP:port with dFPI+use_site=false",
domain: "127.0.0.1",
detailsIn: {
partitionKey: null,
},
expectedOut: {
firstPartyDomain: "",
partitionKey: null,
},
},
{
description: "third-party cookies for IP:port with dFPI+use_site=false",
domain: THIRD_PARTY_DOMAIN,
detailsIn: {
// When use_site=false, the scheme is not stored in the internal
// representation of the partitionKey. So even though the web page
// creates the cookie at HTTP, the cookies are still detected when
// "https" is used.
partitionKey: { topLevelSite: `https://${LOCAL_IP_AND_PORT}` },
},
expectedOut: {
firstPartyDomain: "",
// When use_site=false, the scheme and port are not stored.
// "https" is used as a dummy scheme, and the port is not used.
partitionKey: { topLevelSite: "https://127.0.0.1" },
},
},
];
await runWithPrefs(
[
// Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.
["network.cookie.cookieBehavior", 5],
["privacy.dynamic_firstparty.use_site", false],
],
() => testCookiesAPI({ testCases, topDomain: LOCAL_IP_AND_PORT })
);
});
add_task(async function dfpi_invalid_partitionKey() {
AddonTestUtils.init(globalThis);
AddonTestUtils.createAppInfo(
@ -814,207 +888,6 @@ add_task(async function test_getAll_partitionKey() {
await extension.unload();
});
add_task(async function test_getAll_partitionKey_hasCrossSiteAncestor() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: [
"cookies",
"*://first.example.com/",
"*://third.example.net/",
],
},
async background() {
async function getAllValues(details) {
let cookies = await browser.cookies.getAll(details);
let values = cookies.map(c => c.value);
return values.sort().join(); // Serialize for use with assertEq.
}
const urls = ["http://first.example.com", "http://third.example.net"];
const name = "test_getAll_partitionKey_hasCrossSiteAncestor";
const partitionKeyUnspecified = { topLevelSite: "http://example.com" };
const partitionKeySameSite = {
topLevelSite: "http://example.com",
hasCrossSiteAncestor: false,
};
const partitionKeyForeign = {
topLevelSite: "http://example.com",
hasCrossSiteAncestor: true,
};
try {
for (let url of urls) {
await browser.cookies.set({ url, name, value: "no_partition" });
if (url == "http://first.example.com") {
await browser.cookies.set({
url,
name,
value: "partition1",
partitionKey: partitionKeySameSite,
});
} else {
await browser.test.assertRejects(
browser.cookies.set({
url,
name,
value: "partition1",
partitionKey: partitionKeySameSite,
}),
/Invalid value for 'partitionKey' attribute/,
// When topLevelSite and url are cross-site, hasCrossSiteAncestor cannot be false
"cookies.get should reject invalid partitionKey.topLevelSite"
);
}
await browser.cookies.set({
url,
name,
value: "partition3",
partitionKey: partitionKeyForeign,
});
let cookie = await browser.cookies.set({
url,
name,
value: "partitionX",
partitionKey: partitionKeyUnspecified,
});
if (url == "http://first.example.com") {
browser.test.assertDeepEq(
partitionKeySameSite,
cookie.partitionKey,
"partitionKey.hasCrossSiteAncestor computed as false"
);
browser.test.assertEq(
"no_partition,partition3,partitionX",
await getAllValues({ partitionKey: {} }),
"getAll() with empty partitionKey gets all cookies"
);
browser.test.assertEq(
"partition3,partitionX",
await getAllValues({ partitionKey: partitionKeyUnspecified }),
"getAll() with partitionKey with undefined hasCrossSiteAncestor gets all partitioned cookies"
);
browser.test.assertEq(
"partitionX",
await getAllValues({ partitionKey: partitionKeySameSite }),
"getAll() with partitionKey with false hasCrossSiteAncestor gets cookie stored with undefined hasCrossSiteAncestor"
);
browser.test.assertEq(
"partition3",
await getAllValues({ partitionKey: partitionKeyForeign }),
"getAll() with partitionKey with true hasCrossSiteAncestor gets only cookie stored with true hasCrossSiteAncestor"
);
} else {
browser.test.assertDeepEq(
partitionKeyForeign,
cookie.partitionKey,
"partitionKey.hasCrossSiteAncestor computed as true"
);
browser.test.assertEq(
"no_partition,partitionX",
await getAllValues({ partitionKey: {} }),
"getAll() with empty partitionKey gets all cookies"
);
browser.test.assertEq(
"partitionX",
await getAllValues({ partitionKey: partitionKeyUnspecified }),
"getAll() with partitionKey with undefined hasCrossSiteAncestor gets all partitioned cookies"
);
browser.test.assertEq(
"",
await getAllValues({ partitionKey: partitionKeySameSite }),
"getAll() with partitionKey with false hasCrossSiteAncestor gets no cookies"
);
browser.test.assertEq(
"partitionX",
await getAllValues({ partitionKey: partitionKeyForeign }),
"getAll() with partitionKey with true hasCrossSiteAncestor gets cookie stored with undefined hasCrossSiteAncestor"
);
}
let removedCookie = await browser.cookies.remove({ url, name });
browser.test.assertTrue(
removedCookie,
"remove() without partitionKey removes unpartitioned cookie"
);
removedCookie = await browser.cookies.remove({ url, name });
browser.test.assertDeepEq(
null,
removedCookie,
"remove() without partitionKey does not remove partitioned cookies"
);
removedCookie = await browser.cookies.remove({
url,
name,
partitionKey: partitionKeyUnspecified,
});
browser.test.assertTrue(
removedCookie,
"remove() with partitionKey and unspecified ancestry bit removes the cookie set with that parameter"
);
if (url == "http://first.example.com") {
browser.test.assertDeepEq(
partitionKeySameSite,
removedCookie?.partitionKey,
"remove() with partitionKey and unspecified ancestry bit removes the correct partitioned cookie"
);
removedCookie = await browser.cookies.remove({
url,
name,
partitionKey: partitionKeySameSite,
});
browser.test.assertDeepEq(
null,
removedCookie,
"remove() with same site partition key is the same as the unspecified ancentry for this url"
);
removedCookie = await browser.cookies.remove({
url,
name,
partitionKey: partitionKeyForeign,
});
browser.test.assertTrue(
removedCookie,
"remove() with partitionKey and foreign ancestry bit removes the right cookie"
);
} else {
browser.test.assertDeepEq(
partitionKeyForeign,
removedCookie?.partitionKey,
"remove() with partitionKey and unspecified ancestry bit removes the correct partitioned cookie"
);
await browser.test.assertRejects(
browser.cookies.remove({
url,
name,
partitionKey: partitionKeySameSite,
}),
/Invalid value for 'partitionKey' attribute/,
// When topLevelSite and url are cross-site, hasCrossSiteAncestor cannot be false
"cookies.get should reject invalid partitionKey.topLevelSite"
);
removedCookie = await browser.cookies.remove({
url,
name,
partitionKey: partitionKeyForeign,
});
browser.test.assertDeepEq(
null,
removedCookie,
"remove() with foreign partition key is the same as the unspecified ancentry for this url"
);
}
}
} catch (e) {
browser.test.fail("Unexpected error: " + e);
}
browser.test.sendMessage("test_done");
},
});
await extension.startup();
await extension.awaitMessage("test_done");
await extension.unload();
});
add_task(async function no_unexpected_cookies_at_end_of_test() {
let results = [];
for (const cookie of Services.cookies.cookies) {