summaryrefslogtreecommitdiffstats
path: root/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js')
-rw-r--r--toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js593
1 files changed, 593 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js
new file mode 100644
index 0000000000..7c79ecbe32
--- /dev/null
+++ b/toolkit/components/antitracking/test/browser/browser_partitionedClearSiteDataHeader.js
@@ -0,0 +1,593 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Tests that when receiving the "clear-site-data" header - with dFPI enabled -
+ * we clear storage under the correct partition.
+ */
+
+const { SiteDataTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SiteDataTestUtils.sys.mjs"
+);
+
+const HOST_A = "example.com";
+const HOST_B = "example.org";
+const ORIGIN_A = `https://${HOST_A}`;
+const ORIGIN_B = `https://${HOST_B}`;
+const CLEAR_SITE_DATA_PATH = `/${TEST_PATH}clearSiteData.sjs`;
+const CLEAR_SITE_DATA_URL_ORIGIN_B = ORIGIN_B + CLEAR_SITE_DATA_PATH;
+const CLEAR_SITE_DATA_URL_ORIGIN_A = ORIGIN_A + CLEAR_SITE_DATA_PATH;
+const THIRD_PARTY_FRAME_ID_ORIGIN_B = "thirdPartyFrame";
+const THIRD_PARTY_FRAME_ID_ORIGIN_A = "thirdPartyFrame2";
+const STORAGE_KEY = "testKey";
+
+// Skip localStorage tests when using legacy localStorage. The legacy
+// localStorage implementation does not support clearing data by principal. See
+// Bug 1688221, Bug 1688665.
+const skipLocalStorageTests = Services.prefs.getBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation"
+);
+
+/**
+ * Creates an iframe in the passed browser and waits for it to load.
+ * @param {Browser} browser - Browser to create the frame in.
+ * @param {String} src - Frame source url.
+ * @param {String} id - Frame id.
+ * @param {boolean} sandbox - Whether the frame should be sandboxed.
+ * @returns {Promise} - Resolves once the frame has loaded.
+ */
+function createFrame(browser, src, id, sandbox) {
+ return SpecialPowers.spawn(
+ browser,
+ [{ page: src, frameId: id, sandbox }],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let frame = content.document.createElement("iframe");
+ if (obj.sandbox) {
+ frame.setAttribute("sandbox", "allow-scripts");
+ }
+ frame.src = obj.page;
+ frame.id = obj.frameId;
+ frame.addEventListener("load", resolve, { once: true });
+ content.document.body.appendChild(frame);
+ });
+ }
+ );
+}
+
+/**
+ * Creates a new tab, loads a url and creates an iframe.
+ * Callers need to clean up the tab before the test ends.
+ * @param {String} firstPartyUrl - Url to load in tab.
+ * @param {String} thirdPartyUrl - Url to load in frame.
+ * @param {String} frameId - Id of iframe element.
+ * @param {boolean} sandbox - Whether the frame should be sandboxed.
+ * @returns {Promise} - Resolves with the tab and the frame BrowsingContext once
+ * the tab and the frame have loaded.
+ */
+async function createTabWithFrame(
+ firstPartyUrl,
+ thirdPartyUrl,
+ frameId,
+ sandbox
+) {
+ // Create tab and wait for it to be loaded.
+ let tab = BrowserTestUtils.addTab(gBrowser, firstPartyUrl);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // Create cross origin iframe.
+ await createFrame(tab.linkedBrowser, thirdPartyUrl, frameId, sandbox);
+
+ // Return BrowsingContext of created iframe.
+ return { tab, frameBC: tab.linkedBrowser.browsingContext.children[0] };
+}
+
+/**
+ * Test wrapper for the ClearSiteData tests.
+ * Loads ORIGIN_A and ORIGIN_B in two tabs and inserts a cross origin pointing
+ * to the other iframe each.
+ * Both frames ORIGIN_B under ORIGIN_A and ORIGIN_A under ORIGIN_B will be
+ * storage partitioned.
+ * Depending on the clearDataContext variable we then either navigate ORIGIN_A
+ * (as top level) or ORIGIN_B (as third party frame) to the clear-site-data
+ * endpoint.
+ * @param {function} cbPreClear - Called after initial setup, once top levels
+ * and frames have been loaded.
+ * @param {function} cbPostClear - Called after data has been cleared via the
+ * "Clear-Site-Data" header.
+ * @param {("firstParty"|"thirdPartyPartitioned")} clearDataContext - Whether to
+ * navigate to the path that sets the "Clear-Site-Data" header with the first or
+ * third party.
+ * @param {boolean} [sandboxFrame] - Whether the frames should be sandboxed. No
+ * sandbox by default.
+ */
+async function runClearSiteDataTest(
+ cbPreClear,
+ cbPostClear,
+ clearDataContext,
+ sandboxFrame = false
+) {
+ // Create a tabs for origin A and B with cross origins frames B and A
+ let [
+ { frameBC: frameContextB, tab: tabA },
+ { frameBC: frameContextA, tab: tabB },
+ ] = await Promise.all([
+ createTabWithFrame(
+ ORIGIN_A,
+ ORIGIN_B,
+ THIRD_PARTY_FRAME_ID_ORIGIN_B,
+ sandboxFrame
+ ),
+ createTabWithFrame(
+ ORIGIN_B,
+ ORIGIN_A,
+ THIRD_PARTY_FRAME_ID_ORIGIN_A,
+ sandboxFrame
+ ),
+ ]);
+
+ let browserA = tabA.linkedBrowser;
+ let contextA = browserA.browsingContext;
+ let browserB = tabB.linkedBrowser;
+ let contextB = browserB.browsingContext;
+
+ // Run test callback before clear-site-data
+ if (cbPreClear) {
+ await cbPreClear(contextA, contextB, frameContextB, frameContextA);
+ }
+
+ // Navigate to path with clear-site-data header
+ // Depending on the clearDataContext variable we either do this with the
+ // top browser or the third party storage partitioned frame (B embedded in A).
+ info(`Opening path with clear-site-data-header for ${clearDataContext}`);
+ if (clearDataContext == "firstParty") {
+ // Open in new tab so we keep our current test tab intact. The
+ // post-clear-callback might need it.
+ await BrowserTestUtils.withNewTab(CLEAR_SITE_DATA_URL_ORIGIN_A, () => {});
+ } else if (clearDataContext == "thirdPartyPartitioned") {
+ // Navigate frame to path with clear-site-data header
+ await SpecialPowers.spawn(
+ browserA,
+ [
+ {
+ page: CLEAR_SITE_DATA_URL_ORIGIN_B,
+ frameId: THIRD_PARTY_FRAME_ID_ORIGIN_B,
+ },
+ ],
+ async function (obj) {
+ await new content.Promise(resolve => {
+ let frame = content.document.getElementById(obj.frameId);
+ frame.addEventListener("load", resolve, { once: true });
+ frame.src = obj.page;
+ });
+ }
+ );
+ } else {
+ ok(false, "Invalid context requested for clear-site-data");
+ }
+
+ if (cbPostClear) {
+ await cbPostClear(contextA, contextB, frameContextB, frameContextA);
+ }
+
+ info("Cleaning up.");
+ BrowserTestUtils.removeTab(tabA);
+ BrowserTestUtils.removeTab(tabB);
+ await new Promise(resolve => {
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
+ });
+}
+
+/**
+ * Create an origin with partitionKey.
+ * @param {String} originNoSuffix - Origin without origin attributes.
+ * @param {String} [firstParty] - First party to create partitionKey.
+ * @returns {String} Origin with suffix. If not passed this will return the
+ * umodified origin.
+ */
+function getOrigin(originNoSuffix, firstParty) {
+ let origin = originNoSuffix;
+ if (firstParty) {
+ let [scheme, host] = firstParty.split("://");
+ origin += `^partitionKey=(${scheme},${host})`;
+ }
+ return origin;
+}
+
+/**
+ * Sets a storage item for an origin.
+ * @param {("cookie"|"localStorage")} storageType - Which storage type to use.
+ * @param {String} originNoSuffix - Context to set storage item in.
+ * @param {String} [firstParty] - Optional first party domain to partition
+ * under.
+ * @param {String} key - Key of the entry.
+ * @param {String} value - Value of the entry.
+ */
+function setStorageEntry(storageType, originNoSuffix, firstParty, key, value) {
+ if (storageType != "cookie" && storageType != "localStorage") {
+ ok(false, "Invalid storageType passed");
+ return;
+ }
+
+ let origin = getOrigin(originNoSuffix, firstParty);
+
+ if (storageType == "cookie") {
+ SiteDataTestUtils.addToCookies({ origin, name: key, value });
+ return;
+ }
+ // localStorage
+ SiteDataTestUtils.addToLocalStorage(origin, key, value);
+}
+
+/**
+ * Tests whether a host sets a cookie.
+ * For the purpose of this test we assume that there is either one or no cookie
+ * set.
+ * This performs cookie lookups directly via the cookie service.
+ * @param {boolean} hasCookie - Whether we expect to see a cookie.
+ * @param {String} originNoSuffix - Origin the cookie is stored for.
+ * @param {String|null} firstParty - Whether to test for a partitioned cookie.
+ * If set this will be used to construct the partitionKey.
+ * @param {String} [key] - Expected key / name of the cookie.
+ * @param {String} [value] - Expected value of the cookie.
+ */
+function testHasCookie(hasCookie, originNoSuffix, firstParty, key, value) {
+ let origin = getOrigin(originNoSuffix, firstParty);
+
+ let label = `${originNoSuffix}${
+ firstParty ? ` (partitioned under ${firstParty})` : ""
+ }`;
+
+ if (!hasCookie) {
+ ok(
+ !SiteDataTestUtils.hasCookies(origin),
+ `Cookie for ${label} is not set for key ${key}`
+ );
+ return;
+ }
+
+ ok(
+ SiteDataTestUtils.hasCookies(origin, [{ key, value }]),
+ `Cookie for ${label} is set ${key}=${value}`
+ );
+}
+
+/**
+ * Tests whether a context has a localStorage entry.
+ * @param {boolean} hasEntry - Whether we expect to see an entry.
+ * @param {String} originNoSuffix - Origin to test localStorage for.
+ * @param {String} [firstParty] - First party context to test under.
+ * @param {String} key - key of the localStorage item.
+ * @param {String} [expectedValue] - Expected value of the item.
+ */
+function testHasLocalStorageEntry(
+ hasEntry,
+ originNoSuffix,
+ firstParty,
+ key,
+ expectedValue
+) {
+ if (key == null) {
+ ok(false, "localStorage key is mandatory");
+ return;
+ }
+ let label = `${originNoSuffix}${
+ firstParty ? ` (partitioned under ${firstParty})` : ""
+ }`;
+ let origin = getOrigin(originNoSuffix, firstParty);
+ if (hasEntry) {
+ let hasEntry = SiteDataTestUtils.hasLocalStorage(origin, [
+ { key, value: expectedValue },
+ ]);
+ ok(
+ hasEntry,
+ `localStorage for ${label} has expected value ${key}=${expectedValue}`
+ );
+ } else {
+ let hasEntry = SiteDataTestUtils.hasLocalStorage(origin);
+ ok(!hasEntry, `localStorage for ${label} is not set for key ${key}`);
+ }
+}
+
+/**
+ * Sets the initial storage entries used by the storage tests in this file.
+ * 1. first party ( A )
+ * 2. first party ( B )
+ * 3. third party partitioned ( B under A)
+ * 4. third party partitioned ( A under B)
+ * The entry values reflect which context they are set for.
+ * @param {("cookie"|"localStorage")} storageType - Storage type to initialize.
+ */
+async function setupInitialStorageState(storageType) {
+ if (storageType != "cookie" && storageType != "localStorage") {
+ ok(false, "Invalid storageType passed");
+ return;
+ }
+
+ // Set a first party entry
+ setStorageEntry(storageType, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+
+ // Set a storage entry in the storage partitioned third party frame
+ setStorageEntry(
+ storageType,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+
+ // Set a storage entry in the non storage partitioned third party page
+ setStorageEntry(storageType, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+
+ // Set a storage entry in the second storage partitioned third party frame.
+ setStorageEntry(
+ storageType,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+
+ info("Test that storage entries are set for all contexts");
+
+ if (storageType == "cookie") {
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ return;
+ }
+
+ testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+}
+
+add_setup(async function () {
+ info("Starting ClearSiteData test");
+
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.storage_access.enabled", true],
+ [
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ [
+ "network.cookie.cookieBehavior.pbmode",
+ Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.pbmode.enabled", false],
+ // Needed for SiteDataTestUtils#hasLocalStorage
+ ["dom.storage.client_validation", false],
+ ],
+ });
+});
+
+/**
+ * Test clearing partitioned cookies via clear-site-data header
+ * (Cleared via the cookie service).
+ */
+
+/**
+ * Tests that when a storage partitioned third party frame loads a site with
+ * "Clear-Site-Data", the cookies are cleared for only that partitioned frame.
+ */
+add_task(async function cookieClearThirdParty() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has not changed");
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has been cleared");
+ testHasCookie(false, ORIGIN_B, ORIGIN_A);
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned"
+ );
+});
+
+/**
+ * Tests that when a sandboxed storage partitioned third party frame loads a
+ * site with "Clear-Site-Data", no cookies are cleared and we don't crash.
+ * Crash details in Bug 1686938.
+ */
+add_task(async function cookieClearThirdPartySandbox() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has not changed");
+ testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned",
+ true
+ );
+});
+
+/**
+ * Tests that when the we load a path with "Clear-Site-Data" at top level, the
+ * cookies are cleared only in the first party context.
+ */
+add_task(async function cookieClearFirstParty() {
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("cookie"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party cookie has been cleared");
+ testHasCookie(false, ORIGIN_A, null);
+ info("Testing: Unpartitioned cookie has not changed");
+ testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
+ testHasCookie(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in first party context.
+ "firstParty"
+ );
+});
+
+/**
+ * Test clearing partitioned localStorage via clear-site-data header
+ * (Cleared via the quota manager).
+ */
+
+/**
+ * Tests that when a storage partitioned third party frame loads a site with
+ * "Clear-Site-Data", localStorage is cleared for only that partitioned frame.
+ */
+add_task(async function localStorageClearThirdParty() {
+ // Bug 1688221, Bug 1688665.
+ if (skipLocalStorageTests) {
+ info("Skipping test");
+ return;
+ }
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("localStorage"),
+ // Post Clear-Site-Data
+ async () => {
+ info("Testing: First party localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
+ info("Testing: Unpartitioned localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info(
+ "Testing: Partitioned localStorage for HOST_B (HOST_A) has been cleared"
+ );
+ testHasLocalStorageEntry(false, ORIGIN_B, ORIGIN_A, STORAGE_KEY);
+ info(
+ "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in partitioned third party context.
+ "thirdPartyPartitioned"
+ );
+});
+
+/**
+ * Tests that when the we load a path with "Clear-Site-Data" at top level,
+ * localStorage is cleared only in the first party context.
+ */
+add_task(async function localStorageClearFirstParty() {
+ // Bug 1688221, Bug 1688665.
+ if (skipLocalStorageTests) {
+ info("Skipping test");
+ return;
+ }
+ await runClearSiteDataTest(
+ // Pre Clear-Site-Data
+ () => setupInitialStorageState("localStorage"),
+ // Post Clear-Site-Data
+ () => {
+ info("Testing: First party localStorage has been cleared");
+ testHasLocalStorageEntry(false, ORIGIN_A, null, STORAGE_KEY);
+ info("Testing: Unpartitioned thirdParty localStorage has not changed");
+ testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
+ info(
+ "Testing: Partitioned localStorage for HOST_B (HOST_A) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_B,
+ ORIGIN_A,
+ STORAGE_KEY,
+ "thirdPartyPartitioned"
+ );
+ info(
+ "Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
+ );
+ testHasLocalStorageEntry(
+ true,
+ ORIGIN_A,
+ ORIGIN_B,
+ STORAGE_KEY,
+ "thirdPartyPartitioned2"
+ );
+ },
+ // Send clear-site-data header in first party context.
+ "firstParty"
+ );
+});