Bug 1873418 - Support Partitioned cookie attribute for browser extensions - r=robwu,anti-tracking-reviewers,webdriver-reviewers,timhuang,Sasha

The part that is covered here is extending the partitionKey attribute, as in the proposal of aselya here:

https://github.com/w3c/webextensions/pull/581/files

Notably not included is adding the new Cookies.GetPartitionKey function.

Differential Revision: https://phabricator.services.mozilla.com/D219990
This commit is contained in:
Benjamin VanderSloot 2024-09-26 05:36:00 +00:00
parent ec4e483f35
commit 5ffc34408c
8 changed files with 423 additions and 48 deletions

View File

@ -7,6 +7,7 @@
#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"
@ -1310,27 +1311,68 @@ void ChromeUtils::GetBaseDomainFromPartitionKey(dom::GlobalObject& aGlobal,
/* static */
void ChromeUtils::GetPartitionKeyFromURL(dom::GlobalObject& aGlobal,
const nsAString& aURL,
const nsAString& aTopLevelUrl,
const nsAString& aSubresourceUrl,
const Optional<bool>& aForeignContext,
nsAString& aPartitionKey,
ErrorResult& aRv) {
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
if (NS_SUCCEEDED(rv) && uri->SchemeIs("chrome")) {
nsCOMPtr<nsIURI> topLevelURI;
nsresult rv = NS_NewURI(getter_AddRefs(topLevelURI), aTopLevelUrl);
if (NS_SUCCEEDED(rv) && topLevelURI->SchemeIs("chrome")) {
rv = NS_ERROR_FAILURE;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
aPartitionKey.Truncate();
aRv.Throw(rv);
return;
}
mozilla::OriginAttributes attrs;
// 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);
bool foreignResource;
bool fallback = false;
if (!aSubresourceUrl.IsEmpty()) {
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;
}
// aForeignContext is whether or not this is a foreign context.
// foreignResource is whether or not the resource is cross-site to the top
// level. So we need to validate that a false foreign context doesn't have a
// same-site resource. That is impossible!
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);
aPartitionKey = attrs.mPartitionKey;
}

View File

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

View File

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

View File

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

View File

@ -114,6 +114,8 @@ const TEST_CASES = [
},
];
const SUBRESOURCE_URL = "https://example.net/";
const TEST_INVALID_URLS = [
"",
"/foo",
@ -126,7 +128,10 @@ const TEST_INVALID_URLS = [
add_task(async function test_get_partition_key_from_url() {
for (const test of TEST_CASES) {
info(`Testing url: ${test.url}`);
let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
let partitionKey = ChromeUtils.getPartitionKeyFromURL(
test.url,
SUBRESOURCE_URL
);
Assert.equal(
partitionKey,
@ -141,7 +146,10 @@ add_task(async function test_get_partition_key_from_url_without_site() {
for (const test of TEST_CASES) {
info(`Testing url: ${test.url}`);
let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url);
let partitionKey = ChromeUtils.getPartitionKeyFromURL(
test.url,
SUBRESOURCE_URL
);
Assert.equal(
partitionKey,
@ -194,7 +202,10 @@ add_task(async function test_blob_url() {
return blob_url;
});
let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl);
let partitionKey = ChromeUtils.getPartitionKeyFromURL(
blobUrl,
SUBRESOURCE_URL
);
// The partitionKey of the blob url is empty because the principal of the
// blob url is the JS principal of the global, which doesn't have
@ -214,7 +225,7 @@ add_task(async function test_throw_with_invalid_URL() {
Assert.throws(
() => {
ChromeUtils.getPartitionKeyFromURL(invalidURL);
ChromeUtils.getPartitionKeyFromURL(invalidURL, SUBRESOURCE_URL);
},
/NS_ERROR_MALFORMED_URI/,
"It should fail on invalid URLs."

View File

@ -30,30 +30,60 @@ 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) {
function fromExtPartitionKey(extPartitionKey, cookieUrl) {
if (!extPartitionKey) {
// Unpartitioned by default.
return "";
}
const { topLevelSite } = extPartitionKey;
const { topLevelSite, hasCrossSiteAncestor } = 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 {
return ChromeUtils.getPartitionKeyFromURL(topLevelSite);
// 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
);
} 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 toExtPartitionKey(partitionKey) {
function getExtPartitionKey(cookie) {
let partitionKey = cookie.originAttributes.partitionKey;
if (!partitionKey) {
// Canonical representation of an empty partitionKey is null.
// In theory {topLevelSite: ""} also works, but alas.
@ -65,15 +95,34 @@ function toExtPartitionKey(partitionKey) {
// 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.
return { topLevelSite: `https://${partitionKey}` };
let hasCrossSiteAncestor = !isSubdomain(cookie.host, partitionKey);
return { topLevelSite: `https://${partitionKey}`, hasCrossSiteAncestor };
}
// partitionKey starts with "(" and ends with ")".
let [scheme, domain, port] = partitionKey.slice(1, -1).split(",");
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 topLevelSite = `${scheme}://${domain}`;
if (port) {
topLevelSite += `:${port}`;
}
return { topLevelSite };
// 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 };
}
const convertCookie = ({ cookie, isPrivate }) => {
@ -88,7 +137,7 @@ const convertCookie = ({ cookie, isPrivate }) => {
sameSite: SAME_SITE_STATUSES[cookie.sameSite],
session: cookie.isSession,
firstPartyDomain: cookie.originAttributes.firstPartyDomain || "",
partitionKey: toExtPartitionKey(cookie.originAttributes.partitionKey),
partitionKey: getExtPartitionKey(cookie),
};
if (!cookie.isSession) {
@ -108,10 +157,6 @@ 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) => {
@ -240,7 +285,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),
partitionKey: fromExtPartitionKey(details.partitionKey, details.url),
};
let isPrivate = context.incognito;
@ -270,8 +315,9 @@ const oaFromDetails = (details, context, allowPattern) => {
}
}
// If any of the originAttributes's keys are deleted, this becomes true.
// If any of the originAttributes's keys are deleted, isPattern 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,
@ -292,9 +338,23 @@ 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 };
return {
originAttributes,
isPattern,
isPrivate,
storeId,
topLevelSiteFilter,
};
};
/**
@ -328,7 +388,8 @@ const query = function* (detailsIn, props, context, allowPattern) {
}
throw e;
}
let { originAttributes, isPattern, isPrivate, storeId } = parsedOA;
let { originAttributes, isPattern, isPrivate, storeId, topLevelSiteFilter } =
parsedOA;
if ("domain" in details) {
details.domain = details.domain.toLowerCase().replace(/^\./, "");
@ -430,6 +491,29 @@ 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;
}
@ -653,9 +737,7 @@ this.cookies = class extends ExtensionAPIPersistent {
name: details.name,
storeId,
firstPartyDomain: cookie.originAttributes.firstPartyDomain,
partitionKey: toExtPartitionKey(
cookie.originAttributes.partitionKey
),
partitionKey: getExtPartitionKey(cookie),
});
}

View File

@ -33,6 +33,11 @@
"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,7 +363,10 @@ add_task(async function test_dfpi() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
partitionKey: {
topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}`,
hasCrossSiteAncestor: true,
},
},
},
];
@ -395,7 +398,10 @@ add_task(async function test_dfpi_with_ip_and_port() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` },
partitionKey: {
topLevelSite: `http://${LOCAL_IP_AND_PORT}`,
hasCrossSiteAncestor: true,
},
},
},
];
@ -429,7 +435,10 @@ add_task(async function test_dfpi_with_nested_subdomains() {
},
expectedOut: {
firstPartyDomain: "",
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
partitionKey: {
topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}`,
hasCrossSiteAncestor: true,
},
},
},
];
@ -440,6 +449,10 @@ add_task(async function test_dfpi_with_nested_subdomains() {
);
});
// Bug 1921038:
// This test doesn't actually validate the the behavior it says!
// This is because for these internal partition keys, we cannot return
// the correct scheme!
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
@ -460,13 +473,16 @@ add_task(async function test_dfpi_with_non_default_use_site() {
description: "third-party cookies with dFPI and use_site=false",
domain: THIRD_PARTY_DOMAIN,
detailsIn: {
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
partitionKey: { topLevelSite: `https://${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}` },
partitionKey: {
topLevelSite: `https://${FIRST_DOMAIN_ETLD_PLUS_1}`,
hasCrossSiteAncestor: true,
},
},
},
];
@ -479,6 +495,11 @@ add_task(async function test_dfpi_with_non_default_use_site() {
() => testCookiesAPI({ testCases })
);
});
// Bug 1921038:
// This test doesn't actually validate the the behavior it says!
// This is because for these internal partition keys, we cannot return
// the correct scheme or port!
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
@ -503,13 +524,16 @@ add_task(async function test_dfpi_with_ip_and_port_and_non_default_use_site() {
// 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}` },
partitionKey: { topLevelSite: `https://127.0.0.1` },
},
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" },
partitionKey: {
topLevelSite: `https://127.0.0.1`,
hasCrossSiteAncestor: true,
},
},
},
];
@ -888,6 +912,207 @@ 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) {