forked from mirrors/gecko-dev
895 lines
30 KiB
JavaScript
895 lines
30 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* This test verifies that the extension API's access to cookies is consistent
|
|
* with the cookies as seen by web pages under the following modes:
|
|
* - Every top-level document shares the same cookie jar, every subdocument of
|
|
* the top-level document has a distinct cookie jar tied to the site of the
|
|
* top-level document (dFPI).
|
|
* - All documents have a cookie jar keyed by the domain of the top-level
|
|
* document (FPI).
|
|
* - All cookies are in one cookie jar (classic behavior = no FPI nor dFPI)
|
|
*
|
|
* FPI and dFPI are implemented using OriginAttributes, and historically the
|
|
* consequence of not recognizing an origin attribute is that cookies cannot be
|
|
* deleted. Hence, the functionality of the cookies API is verified as follows,
|
|
* by the testCookiesAPI/runTestCase methods.
|
|
*
|
|
* 1. Load page that creates cookies for the top and a framed document:
|
|
* - "delete_me"
|
|
* - "edit_me"
|
|
* 2. cookies.getAll: get all cookies with extension API.
|
|
* 3. cookies.remove: Remove "delete_me" cookies with the extension API.
|
|
* 4. cookies.set: Edit "edit_me" cookie with the extension API.
|
|
* 5. Verify that the web page can see "edit_me" cookie (via document.cookie).
|
|
* 6. cookies.get: "edit_me" is still present.
|
|
* 7. cookies.remove: "edit_me" can be removed.
|
|
* 8. cookies.getAll: no cookies left.
|
|
*/
|
|
|
|
const FIRST_DOMAIN = "first.example.com";
|
|
const FIRST_DOMAIN_ETLD_PLUS_1 = "example.com";
|
|
const FIRST_DOMAIN_ETLD_PLUS_MANY = "nested.under.first.example.com";
|
|
const THIRD_PARTY_DOMAIN = "third.example.net";
|
|
const server = createHttpServer({
|
|
hosts: [FIRST_DOMAIN, FIRST_DOMAIN_ETLD_PLUS_MANY, THIRD_PARTY_DOMAIN],
|
|
});
|
|
const LOCAL_IP_AND_PORT = `127.0.0.1:${server.identity.primaryPort}`;
|
|
|
|
server.registerPathHandler("/top", (request, response) => {
|
|
response.setHeader("Set-Cookie", `delete_me=top; SameSite=none`);
|
|
response.setHeader("Set-Cookie", `edit_me=top; SameSite=none`, true);
|
|
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
|
|
response.write(
|
|
`<!DOCTYPE html><iframe src="//third.example.net/framed"></iframe>`
|
|
);
|
|
});
|
|
server.registerPathHandler("/framed", (request, response) => {
|
|
response.setHeader("Set-Cookie", `delete_me=frame; SameSite=none`);
|
|
response.setHeader("Set-Cookie", `edit_me=frame; SameSite=none`, true);
|
|
});
|
|
|
|
// Background script of the extension that drives the test.
|
|
// It first waits for the content scripts in /top and /framed to connect,
|
|
// in order to verify that cookie operations by the extension API are reflected
|
|
// to the web page (verified through document.cookie from the content script).
|
|
function backgroundScript() {
|
|
let portsByDomain = new Map();
|
|
|
|
async function getDocumentCookies(port) {
|
|
return new Promise(resolve => {
|
|
port.onMessage.addListener(function listener(cookieString) {
|
|
port.onMessage.removeListener(listener);
|
|
resolve(cookieString);
|
|
});
|
|
port.postMessage("get_cookies");
|
|
});
|
|
}
|
|
|
|
// Stringify cookie identifier for comparisons in assertions.
|
|
function stringifyCookie(cookie) {
|
|
if (!cookie) {
|
|
return "COOKIE MISSING";
|
|
}
|
|
let domain = cookie.domain;
|
|
if (!domain) {
|
|
// The return value of `cookies.remove` has a URL instead of a domain.
|
|
domain = new URL(cookie.url).hostname;
|
|
}
|
|
return `${cookie.name} domain=${domain} firstPartyDomain=${
|
|
cookie.firstPartyDomain
|
|
} partitionKey=${JSON.stringify(cookie.partitionKey)}`;
|
|
}
|
|
function stringifyCookies(cookies) {
|
|
return cookies.map(stringifyCookie).sort().join(" , ");
|
|
}
|
|
|
|
// detailsIn may have partitionKey and firstPartyDomain attributes.
|
|
// expectedOut has partitionKey and firstPartyDomain attributes.
|
|
async function runTestCase({ domain, detailsIn, expectedOut }) {
|
|
const port = portsByDomain.get(domain);
|
|
browser.test.assertTrue(port, `Got port to document for ${domain}`);
|
|
|
|
let allCookies = await browser.cookies.getAll({
|
|
domain,
|
|
firstPartyDomain: null,
|
|
partitionKey: {},
|
|
});
|
|
|
|
let allCookiesWithFPD = await browser.cookies.getAll({
|
|
domain,
|
|
...detailsIn,
|
|
});
|
|
browser.test.assertEq(
|
|
stringifyCookies(allCookies),
|
|
stringifyCookies(allCookiesWithFPD),
|
|
"cookies.getAll returns consistent results"
|
|
);
|
|
|
|
for (let [key, expectedValue] of Object.entries(expectedOut)) {
|
|
expectedValue = JSON.stringify(expectedValue);
|
|
browser.test.assertTrue(
|
|
allCookies.every(c => JSON.stringify(c[key]) === expectedValue),
|
|
`All ${allCookies.length} cookies have ${key}=${expectedValue}`
|
|
);
|
|
}
|
|
|
|
// delete_me: get, remove, get.
|
|
const cookieToDelete = {
|
|
url: `http://${domain}/`,
|
|
name: "delete_me",
|
|
...detailsIn,
|
|
};
|
|
const deletedCookie = {
|
|
...cookieToDelete,
|
|
...expectedOut,
|
|
};
|
|
browser.test.assertEq(
|
|
stringifyCookie(deletedCookie),
|
|
stringifyCookie(await browser.cookies.get(cookieToDelete)),
|
|
"delete_me cookie exists before removal"
|
|
);
|
|
browser.test.assertEq(
|
|
stringifyCookie(deletedCookie),
|
|
stringifyCookie(await browser.cookies.remove(cookieToDelete)),
|
|
"delete_me cookie has been removed by cookies.remove"
|
|
);
|
|
browser.test.assertEq(
|
|
null,
|
|
await browser.cookies.get(cookieToDelete),
|
|
"delete_me cookie does not exist any more"
|
|
);
|
|
|
|
// edit_me: set, retrieve via document.cookie
|
|
const cookieToEdit = {
|
|
url: `http://${domain}/`,
|
|
name: "edit_me",
|
|
...detailsIn,
|
|
};
|
|
const editedCookie = await browser.cookies.set({
|
|
...cookieToEdit,
|
|
value: `new_value_${domain}`,
|
|
});
|
|
browser.test.assertEq(
|
|
stringifyCookie({ ...cookieToEdit, ...expectedOut }),
|
|
stringifyCookie(editedCookie),
|
|
"edit_me cookie updated"
|
|
);
|
|
browser.test.assertEq(
|
|
await getDocumentCookies(port),
|
|
`edit_me=new_value_${domain}`,
|
|
"Expected cookies after removing and editing a cookie"
|
|
);
|
|
|
|
// edit_me: get, remove, getAll.
|
|
browser.test.assertEq(
|
|
stringifyCookie(editedCookie),
|
|
stringifyCookie(await browser.cookies.get(cookieToEdit)),
|
|
"edit_me cookie still exists"
|
|
);
|
|
await browser.cookies.remove(cookieToEdit);
|
|
let allCookiesAtEnd = await browser.cookies.getAll({
|
|
domain,
|
|
firstPartyDomain: null,
|
|
partitionKey: {},
|
|
});
|
|
browser.test.assertEq(
|
|
"[]",
|
|
JSON.stringify(allCookiesAtEnd),
|
|
"No cookies left"
|
|
);
|
|
}
|
|
|
|
let resolveTestReady;
|
|
let testReadyPromise = new Promise(resolve => {
|
|
resolveTestReady = resolve;
|
|
});
|
|
|
|
browser.test.onMessage.addListener(async (msg, testCase) => {
|
|
await testReadyPromise;
|
|
browser.test.assertEq("runTest", msg, `Starting: ${testCase.description}`);
|
|
try {
|
|
await runTestCase(testCase);
|
|
} catch (e) {
|
|
browser.test.fail(`Unexpected error: ${e} :: ${e.stack}`);
|
|
}
|
|
browser.test.sendMessage("runTest_done");
|
|
});
|
|
|
|
// cookie-checker-contentscript.js will connect.
|
|
browser.runtime.onConnect.addListener(port => {
|
|
portsByDomain.set(port.name, port);
|
|
browser.test.log(`Got port #${portsByDomain.size} ${port.name}`);
|
|
if (portsByDomain.size === 2) {
|
|
// The top document and the embedded frame has loaded and the
|
|
// content script that we use to read cookies is connected.
|
|
// The test can now start.
|
|
resolveTestReady();
|
|
}
|
|
});
|
|
}
|
|
|
|
// The primary purpose of this test is to verify that the cookies API can read
|
|
// and write cookies that are actually in use by the web page.
|
|
async function testCookiesAPI({ testCases, topDomain = FIRST_DOMAIN }) {
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
background: backgroundScript,
|
|
manifest: {
|
|
permissions: [
|
|
"cookies",
|
|
// Remove port to work around bug 1350523.
|
|
`*://${topDomain.replace(/:\d+$/, "")}/*`,
|
|
`*://${THIRD_PARTY_DOMAIN}/*`,
|
|
],
|
|
content_scripts: [
|
|
{
|
|
js: ["cookie-checker-contentscript.js"],
|
|
matches: [
|
|
// Remove port to work around bug 1362809.
|
|
`*://${topDomain.replace(/:\d+$/, "")}/top`,
|
|
`*://${THIRD_PARTY_DOMAIN}/framed`,
|
|
],
|
|
all_frames: true,
|
|
run_at: "document_end",
|
|
},
|
|
],
|
|
},
|
|
files: {
|
|
"cookie-checker-contentscript.js": () => {
|
|
const port = browser.runtime.connect({ name: location.hostname });
|
|
port.onMessage.addListener(msg => {
|
|
browser.test.assertEq(msg, "get_cookies", "Expected port message");
|
|
port.postMessage(document.cookie);
|
|
});
|
|
},
|
|
},
|
|
});
|
|
await extension.startup();
|
|
let contentPage = await ExtensionTestUtils.loadContentPage(
|
|
`http://${topDomain}/top`
|
|
);
|
|
for (let testCase of testCases) {
|
|
info(`Running test case: ${testCase.description}`);
|
|
extension.sendMessage("runTest", testCase);
|
|
await extension.awaitMessage("runTest_done");
|
|
}
|
|
await contentPage.close();
|
|
await extension.unload();
|
|
}
|
|
|
|
add_task(async function setup() {
|
|
// SameSite=none is needed to set cookies in third-party contexts.
|
|
// SameSite=none usually requires Secure, but the test server doesn't support
|
|
// https, so disable the Secure requirement for SameSite=none.
|
|
Services.prefs.setBoolPref(
|
|
"network.cookie.sameSite.noneRequiresSecure",
|
|
false
|
|
);
|
|
});
|
|
|
|
add_task(async function test_no_partitioning() {
|
|
const testCases = [
|
|
{
|
|
description: "first-party cookies without any partitioning",
|
|
domain: FIRST_DOMAIN,
|
|
detailsIn: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
{
|
|
description: "third-party cookies without any partitioning",
|
|
domain: THIRD_PARTY_DOMAIN,
|
|
detailsIn: {
|
|
// Without (d)FPI, firstPartyDomain and partitionKey are optional.
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
];
|
|
await runWithPrefs(
|
|
// dFPI is enabled by default on Nightly, disable it.
|
|
[["network.cookie.cookieBehavior", 4]],
|
|
() => testCookiesAPI({ testCases })
|
|
);
|
|
});
|
|
|
|
add_task(async function test_firstPartyIsolate() {
|
|
const testCases = [
|
|
{
|
|
description: "first-party cookies with FPI",
|
|
domain: FIRST_DOMAIN,
|
|
detailsIn: {
|
|
firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1,
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1,
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
{
|
|
description: "third-party cookies with FPI",
|
|
domain: THIRD_PARTY_DOMAIN,
|
|
detailsIn: {
|
|
firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1,
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: FIRST_DOMAIN_ETLD_PLUS_1,
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
];
|
|
await runWithPrefs(
|
|
[
|
|
// FPI is mutually exclusive with dFPI. Disable dFPI.
|
|
["network.cookie.cookieBehavior", 4],
|
|
["privacy.firstparty.isolate", true],
|
|
],
|
|
() => testCookiesAPI({ testCases })
|
|
);
|
|
});
|
|
|
|
add_task(async function test_dfpi() {
|
|
const testCases = [
|
|
{
|
|
description: "first-party cookies with dFPI",
|
|
domain: FIRST_DOMAIN,
|
|
detailsIn: {
|
|
// partitionKey is optional and expected to default to unpartitioned.
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
{
|
|
description: "third-party cookies with dFPI",
|
|
domain: THIRD_PARTY_DOMAIN,
|
|
detailsIn: {
|
|
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
|
|
},
|
|
},
|
|
];
|
|
await runWithPrefs(
|
|
// Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.
|
|
[["network.cookie.cookieBehavior", 5]],
|
|
() => testCookiesAPI({ testCases })
|
|
);
|
|
});
|
|
|
|
add_task(async function test_dfpi_with_ip_and_port() {
|
|
const testCases = [
|
|
{
|
|
description: "first-party cookies for IP with port",
|
|
domain: "127.0.0.1",
|
|
detailsIn: {
|
|
partitionKey: null,
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
{
|
|
description: "third-party cookies for IP with port",
|
|
domain: THIRD_PARTY_DOMAIN,
|
|
detailsIn: {
|
|
partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` },
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: { topLevelSite: `http://${LOCAL_IP_AND_PORT}` },
|
|
},
|
|
},
|
|
];
|
|
await runWithPrefs(
|
|
// Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.
|
|
[["network.cookie.cookieBehavior", 5]],
|
|
() => testCookiesAPI({ testCases, topDomain: LOCAL_IP_AND_PORT })
|
|
);
|
|
});
|
|
|
|
add_task(async function test_dfpi_with_nested_subdomains() {
|
|
const testCases = [
|
|
{
|
|
description: "first-party cookies with DFPI at eTLD+many",
|
|
domain: FIRST_DOMAIN_ETLD_PLUS_MANY,
|
|
detailsIn: {
|
|
partitionKey: null,
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: null,
|
|
},
|
|
},
|
|
{
|
|
description: "third-party cookies for first party with eTLD+many",
|
|
domain: THIRD_PARTY_DOMAIN,
|
|
detailsIn: {
|
|
// Partitioned cookies are keyed by eTLD+1, so even if eTLD+many is
|
|
// passed, then eTLD+1 is stored (and returned).
|
|
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_MANY}` },
|
|
},
|
|
expectedOut: {
|
|
firstPartyDomain: "",
|
|
partitionKey: { topLevelSite: `http://${FIRST_DOMAIN_ETLD_PLUS_1}` },
|
|
},
|
|
},
|
|
];
|
|
await runWithPrefs(
|
|
// Enable dFPI; 5 = BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN.
|
|
[["network.cookie.cookieBehavior", 5]],
|
|
() => testCookiesAPI({ testCases, topDomain: FIRST_DOMAIN_ETLD_PLUS_MANY })
|
|
);
|
|
});
|
|
|
|
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(
|
|
"xpcshell@tests.mozilla.org",
|
|
"XPCShell",
|
|
"1",
|
|
"42"
|
|
);
|
|
// The test below uses the browser.privacy API, which relies on
|
|
// ExtensionSettingsStore, which in turn depends on AddonManager.
|
|
await AddonTestUtils.promiseStartupManager();
|
|
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
useAddonManager: "temporary",
|
|
manifest: {
|
|
permissions: ["cookies", "*://example.com/*", "privacy"],
|
|
},
|
|
async background() {
|
|
const url = "http://example.com/";
|
|
const name = "dfpi_invalid_partitionKey_dummy_name";
|
|
const value = "1";
|
|
|
|
// Shorthands to minimize boilerplate.
|
|
const set = d => browser.cookies.set({ url, name, value, ...d });
|
|
const remove = d => browser.cookies.remove({ url, name, ...d });
|
|
const get = d => browser.cookies.get({ url, name, ...d });
|
|
const getAll = d => browser.cookies.getAll(d);
|
|
|
|
await browser.test.assertRejects(
|
|
set({ partitionKey: { topLevelSite: "example.net" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"partitionKey must be a URL, not a domain"
|
|
);
|
|
await browser.test.assertRejects(
|
|
set({ partitionKey: { topLevelSite: "chrome://foo" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"partitionKey cannot be the chrome:-scheme (canonicalization fails)"
|
|
);
|
|
await browser.test.assertRejects(
|
|
set({ partitionKey: { topLevelSite: "chrome://foo/foo/foo" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"partitionKey cannot be the chrome:-scheme (canonicalization passes)"
|
|
);
|
|
await browser.test.assertRejects(
|
|
set({ partitionKey: { topLevelSite: "http://[]:" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"partitionKey must be a valid URL"
|
|
);
|
|
|
|
browser.test.assertThrows(
|
|
() => get({ partitionKey: "" }),
|
|
/Error processing partitionKey: Expected object instead of ""/,
|
|
"cookies.get should reject invalid partitionKey (string)"
|
|
);
|
|
browser.test.assertThrows(
|
|
() => get({ partitionKey: { topLevelSite: "http://x", badkey: 0 } }),
|
|
/Error processing partitionKey: Unexpected property "badkey"/,
|
|
"cookies.get should reject unsupported keys in partitionKey"
|
|
);
|
|
await browser.test.assertRejects(
|
|
remove({ partitionKey: { topLevelSite: "invalid" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"cookies.remove should reject invalid partitionKey.topLevelSite"
|
|
);
|
|
await browser.test.assertRejects(
|
|
get({ partitionKey: { topLevelSite: "invalid" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"cookies.get should reject invalid partitionKey.topLevelSite"
|
|
);
|
|
await browser.test.assertRejects(
|
|
getAll({ partitionKey: { topLevelSite: "invalid" } }),
|
|
/Invalid value for 'partitionKey' attribute/,
|
|
"cookies.getAll should reject invalid partitionKey.topLevelSite"
|
|
);
|
|
|
|
// firstPartyDomain and partitionKey are mutually exclusive, because
|
|
// FPI and dFPI are mutually exclusive.
|
|
await browser.test.assertRejects(
|
|
set({ firstPartyDomain: "example.net", partitionKey: {} }),
|
|
/Partitioned cookies cannot have a 'firstPartyDomain' attribute./,
|
|
"partitionKey and firstPartyDomain cannot both be non-empty"
|
|
);
|
|
|
|
// On Nightly, dFPI is enabled by default. We have to disable it first,
|
|
// before we can enable FPI. Otherwise we would get error:
|
|
// Can't enable firstPartyIsolate when cookieBehavior is 'reject_trackers_and_partition_foreign'
|
|
await browser.privacy.websites.cookieConfig.set({
|
|
value: { behavior: "reject_trackers" },
|
|
});
|
|
await browser.privacy.websites.firstPartyIsolate.set({
|
|
value: true,
|
|
});
|
|
|
|
// FPI and dFPI are mutually exclusive. FPI is documented to require the
|
|
// firstPartyDomain attribute, let's verify that, despite it being
|
|
// technically possible to support both attributes.
|
|
for (let cookiesMethod of [get, getAll, remove, set]) {
|
|
await browser.test.assertRejects(
|
|
cookiesMethod({ partitionKey: { topLevelSite: url } }),
|
|
/First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set./,
|
|
`cookies.${cookiesMethod.name} requires firstPartyDomain when FPI is enabled`
|
|
);
|
|
}
|
|
|
|
// The pref changes above (to dFPI/FPI) via the browser.privacy API will
|
|
// be undone when the extension unloads.
|
|
|
|
browser.test.sendMessage("test_done");
|
|
},
|
|
});
|
|
await extension.startup();
|
|
await extension.awaitMessage("test_done");
|
|
await extension.unload();
|
|
|
|
await AddonTestUtils.promiseShutdownManager();
|
|
});
|
|
|
|
add_task(async function dfpi_moz_extension() {
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
manifest: {
|
|
permissions: ["cookies", "*://example.com/*"],
|
|
},
|
|
async background() {
|
|
let cookie = await browser.cookies.set({
|
|
url: "http://example.com/",
|
|
name: "moz_ext_party",
|
|
value: "1",
|
|
// moz-extension: URL is passed here, in an attempt to mark the cookie
|
|
// as part of the "moz-extension:"-partition. Below we will expect ""
|
|
// because the dFPI implementation treats "moz-extension" as
|
|
// unpartitioned, see
|
|
// https://searchfox.org/mozilla-central/rev/ac7da6c7306d86e2f86a302ce1e170ad54b3c1fe/caps/OriginAttributes.cpp#79-82
|
|
partitionKey: { topLevelSite: browser.runtime.getURL("/") },
|
|
});
|
|
browser.test.assertEq(
|
|
null,
|
|
cookie.partitionKey,
|
|
"Cookies in moz-extension:-URL are unpartitioned"
|
|
);
|
|
let deletedCookie = await browser.cookies.remove({
|
|
url: "http://example.com/",
|
|
name: "moz_ext_party",
|
|
partitionKey: { topLevelSite: "moz-extension://ignoreme" },
|
|
});
|
|
browser.test.assertEq(
|
|
null,
|
|
deletedCookie.partitionKey,
|
|
"moz-extension:-partition key is treated as unpartitioned"
|
|
);
|
|
browser.test.sendMessage("test_done");
|
|
},
|
|
});
|
|
await extension.startup();
|
|
await extension.awaitMessage("test_done");
|
|
await extension.unload();
|
|
});
|
|
|
|
add_task(async function dfpi_about_scheme_as_partitionKey() {
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
manifest: {
|
|
permissions: ["cookies", "*://example.com/*"],
|
|
},
|
|
async background() {
|
|
let cookie = await browser.cookies.set({
|
|
url: "http://example.com/",
|
|
name: "moz_ext_party",
|
|
value: "1",
|
|
partitionKey: { topLevelSite: "about:blank" },
|
|
});
|
|
// It doesn't really make sense to partition in `about:blank` (since it
|
|
// cannot really be a first party), but for completeness of test coverage
|
|
// we also check that the use of an about:-scheme results in predictable
|
|
// behavior. The weird "about://"-URL below is the serialization of the
|
|
// internal value of the partitionKey attribute:
|
|
// https://searchfox.org/mozilla-central/rev/ac7da6c7306d86e2f86a302ce1e170ad54b3c1fe/caps/OriginAttributes.cpp#73-77
|
|
browser.test.assertEq(
|
|
"about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
|
|
cookie.partitionKey.topLevelSite,
|
|
"An URL-like representation of the internal about:-format is returned"
|
|
);
|
|
let deletedCookie = await browser.cookies.remove({
|
|
url: "http://example.com/",
|
|
name: "moz_ext_party",
|
|
partitionKey: {
|
|
topLevelSite:
|
|
"about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
|
|
},
|
|
});
|
|
browser.test.assertEq(
|
|
"about://about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla",
|
|
deletedCookie.partitionKey.topLevelSite,
|
|
"Cookie can be deleted via the dummy about:-scheme"
|
|
);
|
|
browser.test.sendMessage("test_done");
|
|
},
|
|
});
|
|
await extension.startup();
|
|
await extension.awaitMessage("test_done");
|
|
await extension.unload();
|
|
});
|
|
|
|
// Same-site frames are expected to be unpartitioned.
|
|
// The cookies API can receive partitionKey and url that are same-site. While
|
|
// such cookies won't be sent to websites in practice, we do want to verify that
|
|
// the behavior is predictable.
|
|
add_task(async function test_url_is_same_site_as_partitionKey() {
|
|
// This loads a page with a frame at third.example.net (= THIRD_PARTY_DOMAIN).
|
|
let contentPage = await ExtensionTestUtils.loadContentPage(
|
|
`http://${THIRD_PARTY_DOMAIN}/top`
|
|
);
|
|
await contentPage.close();
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
manifest: {
|
|
permissions: ["cookies", "*://third.example.net/"],
|
|
},
|
|
async background() {
|
|
// Retrieve all cookies, partitioned and unpartitioned. We expect only
|
|
// unpartitioned cookies at first because the top frame and the child
|
|
// frame have the same origin.
|
|
let initialCookies = await browser.cookies.getAll({ partitionKey: {} });
|
|
browser.test.assertEq(
|
|
"delete_me=frame,edit_me=frame",
|
|
initialCookies.map(c => `${c.name}=${c.value}`).join(),
|
|
"Same-site frames are in unpartitioned storage; /frame overwrites /top"
|
|
);
|
|
browser.test.assertTrue(
|
|
await browser.cookies.remove({
|
|
url: "https://third.example.net/",
|
|
name: "delete_me",
|
|
}),
|
|
"Removed unpartitioned cookie"
|
|
);
|
|
browser.test.assertEq(
|
|
"[null,null]",
|
|
JSON.stringify(initialCookies.map(c => c.partitionKey)),
|
|
"Cookies in same-site/same-origin frames are not partitioned"
|
|
);
|
|
|
|
// We only have one unpartitioned cookie (edit_cookie) left.
|
|
|
|
// Add new cookie whose partitionKey is same-site relative to url.
|
|
let newCookie = await browser.cookies.set({
|
|
url: "http://third.example.net/",
|
|
name: "edit_me",
|
|
value: "url_is_partitionKey_eTLD+2",
|
|
partitionKey: { topLevelSite: "http://third.example.net" },
|
|
});
|
|
browser.test.assertEq(
|
|
"http://example.net",
|
|
newCookie.partitionKey.topLevelSite,
|
|
"Created cookie with partitionKey=url; eTLD+2 is normalized as eTLD+1"
|
|
);
|
|
|
|
browser.test.assertTrue(
|
|
await browser.cookies.remove({
|
|
url: "http://third.example.net/",
|
|
name: "edit_me",
|
|
partitionKey: {},
|
|
}),
|
|
"Removed unpartitioned cookie when partitionKey: {} is used"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
null,
|
|
await browser.cookies.remove({
|
|
url: "http://third.example.net/",
|
|
name: "edit_me",
|
|
partitionKey: {},
|
|
}),
|
|
"No more unpartitioned cookies to remove"
|
|
);
|
|
|
|
browser.test.assertTrue(
|
|
await browser.cookies.remove({
|
|
url: "http://third.example.net/",
|
|
name: "edit_me",
|
|
partitionKey: { topLevelSite: "http://example.net" },
|
|
}),
|
|
"Removed partitioned cookie when partitionKey is passed"
|
|
);
|
|
|
|
browser.test.sendMessage("test_done");
|
|
},
|
|
});
|
|
await extension.startup();
|
|
await extension.awaitMessage("test_done");
|
|
await extension.unload();
|
|
});
|
|
|
|
add_task(async function test_getAll_partitionKey() {
|
|
let extension = ExtensionTestUtils.loadExtension({
|
|
manifest: {
|
|
permissions: ["cookies", "*://third.example.net/"],
|
|
},
|
|
async background() {
|
|
const url = "http://third.example.net";
|
|
const name = "test_url_is_identical_to_partitionKey";
|
|
const partitionKey = { topLevelSite: "http://example.com" };
|
|
const firstPartyDomain = "example.net";
|
|
|
|
// Create non-partitioned cookie, create partitioned cookie.
|
|
await browser.cookies.set({ url, name, value: "no_partition" });
|
|
await browser.cookies.set({ url, name, value: "fpd", firstPartyDomain });
|
|
await browser.cookies.set({ url, name, partitionKey, value: "party" });
|
|
// partitionKey + firstPartyDomain was tested in dfpi_invalid_partitionKey
|
|
|
|
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.
|
|
}
|
|
|
|
browser.test.assertEq(
|
|
"no_partition",
|
|
await getAllValues({}),
|
|
"getAll() returns unpartitioned by default"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
"no_partition,party",
|
|
await getAllValues({ partitionKey: {} }),
|
|
"getAll() with partitionKey: {} returns all cookies"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
"party",
|
|
await getAllValues({ partitionKey }),
|
|
"getAll() with specific partitionKey returns partitionKey cookies only"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
"",
|
|
await getAllValues({ partitionKey: { topLevelSite: url } }),
|
|
"getAll() with partitionKey set to cookie URL does not match anything"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
"",
|
|
await getAllValues({ partitionKey, firstPartyDomain }),
|
|
"getAll() with non-empty partitionKey and firstPartyDomain does not match anything"
|
|
);
|
|
browser.test.assertEq(
|
|
"fpd",
|
|
await getAllValues({ partitionKey: {}, firstPartyDomain }),
|
|
"getAll() with empty partitionKey and firstPartyDomain matches fpd"
|
|
);
|
|
|
|
browser.test.assertEq(
|
|
"fpd,no_partition,party",
|
|
await getAllValues({ partitionKey: {}, firstPartyDomain: null }),
|
|
"getAll() with empty partitionKey and firstPartyDomain:null matches everything"
|
|
);
|
|
|
|
await browser.cookies.remove({ url, name });
|
|
await browser.cookies.remove({ url, name, firstPartyDomain });
|
|
await browser.cookies.remove({ url, name, partitionKey });
|
|
|
|
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) {
|
|
results.push({
|
|
name: cookie.name,
|
|
value: cookie.value,
|
|
host: cookie.host,
|
|
originAttributes: cookie.originAttributes,
|
|
});
|
|
}
|
|
Assert.deepEqual(results, [], "Test should not leave any cookies");
|
|
});
|