diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/antitracking/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/antitracking/test')
201 files changed, 28478 insertions, 0 deletions
diff --git a/toolkit/components/antitracking/test/browser/.eslintrc.js b/toolkit/components/antitracking/test/browser/.eslintrc.js new file mode 100644 index 0000000000..e57058ecb1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/toolkit/components/antitracking/test/browser/3rdParty.html b/toolkit/components/antitracking/test/browser/3rdParty.html new file mode 100644 index 0000000000..7cb88ea17a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdParty.html @@ -0,0 +1,53 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let data = e.data; + if (data.includes("!!!")) { + // The data argument may be packed with information about whether we are on + // the allow list. In that case, extract that information and prepare it + // for our callbacks to access it. + let parts = data.split("!!!"); + // Only consider ourselves allow-listed when the cookie policy is set to + // 'block third-party trackers or 'block third-party trackers and partition + // third-party cookies', since otherwise we won't obtain storage access by + // default, which is what this data is used for in tests. + let cookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior.pbmode") + : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior"); + window.allowListed = + parts[0] === "true" && + (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER || + cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN || + (cookieBehavior == SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN && + SpecialPowers.Services.prefs.getBoolPref("network.cookie.rejectForeignWithExceptions.enabled"))); + data = parts[1]; + } + let runnableStr = `(() => {return (${data});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, /* Phase */ 1).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpen.html b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html new file mode 100644 index 0000000000..1110834c0e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyOpen.html @@ -0,0 +1,16 @@ +<html> +<head> + <title>A popup!</title> +</head> +<body> +<h1>hi!</h1> +<script> + +if (opener) { + opener.postMessage("hello!", "*"); +} +window.close(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html new file mode 100644 index 0000000000..5afbb5d89b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html @@ -0,0 +1,17 @@ +<html> +<head> + <title>A popup!</title> +</head> +<body> +<h1>hi!</h1> +<script> + +SpecialPowers.wrap(document).userInteractionForTesting(); +if (window.location.search == "?messageme") { + window.opener.postMessage("done", "*"); +} +window.close(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html new file mode 100644 index 0000000000..f97ed4791f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyPartitioned.html @@ -0,0 +1,29 @@ +<html> +<head> + <title>3rd party content!</title> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +onmessage = async function(e) { + let cb = e.data.cb; + let runnableStr = `(() => {return (${cb});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let variant = (new URL(location.href)).searchParams.get("variant"); + let win = this; + if (variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(this, win, e.data.value); + parent.postMessage(result, "*"); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyRelay.html b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html new file mode 100644 index 0000000000..64e713913b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyRelay.html @@ -0,0 +1,41 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Relay</h1> +<iframe></iframe> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + switch (e.data.type || "") { + case "finish": + case "ok": + case "info": + parent.postMessage(e.data, "*"); + break; + default: + let iframe = document.querySelector("iframe"); + iframe.contentWindow.postMessage(e.data, "*"); + break; + } +}; + +document.querySelector("iframe").src = location.search.substr(1); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartySVG.html b/toolkit/components/antitracking/test/browser/3rdPartySVG.html new file mode 100644 index 0000000000..df791f355f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartySVG.html @@ -0,0 +1,20 @@ +<html> +<head> + <title>3rd party content!</title> + <style> + body { + background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="context-fill" d="M28.5,8.1c0-1.1-1-1.9-2.1-2.4V3.7c-0.2-0.2-0.3-0.3-0.6-0.3c-0.6,0-1.1,0.8-1.3,2.1c-0.2,0-0.3,0-0.5,0l0,0c0-0.2,0-0.3-0.2-0.5c-0.3-1.1-0.8-1.9-1.3-2.6C22,2.6,21.7,3.2,21.7,4L22,6.3c-0.3,0.2-0.6,0.3-1,0.6l-3.5,3.7l0,0c0,0-6.3-0.8-10.9,0.2c-0.6,0-1,0.2-1.1,0.3c-0.5,0.2-0.8,0.3-1.1,0.6c-1.1-0.8-2.2-2.1-3.2-4c0-0.3-0.5-0.5-0.8-0.5s-0.5,0.6-0.3,1c0.8,2.1,2.1,3.5,3.4,4.5c-0.5,0.5-0.8,1-1,1.6c0,0-0.3,2.2-0.3,5.5l1.4,8c0,1,0.8,1.8,1.9,1.8c1,0,1.9-0.8,1.9-1.8V23l0.5-1.3h8.8l0.8,1.3v4.7c0,1,0.8,1.8,1.9,1.8c1,0,1.6-0.6,1.8-1.4l0,0l1.9-9l0,0l2.1-6.4h3c3.4,0,3.7-2.9,3.7-2.9L28.5,8.1z"/></svg>'); + } + </style> +</head> +<body> +<h1>3rd party content with an SVG image background</h1> +<script> + +onload = function(e) { + parent.postMessage({ type: "finish" }, "*"); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorage.html b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html new file mode 100644 index 0000000000..749ead7c20 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyStorage.html @@ -0,0 +1,44 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let data = e.data; + let runnableStr = `(() => {return (${data});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + + let win = window.open("3rdPartyStorageWO.html"); + win.onload = async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await runnable.call(this, this, win, false /* allowed */); + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + await runnable.call(this, this, win, true /* allowed */); + + win.close(); + parent.postMessage({ type: "finish" }, "*"); + }; +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html new file mode 100644 index 0000000000..b04916103d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyStorageWO.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>1st party content!</title> +</head> +<body> +<h1>Here the 1st party content!</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyUI.html b/toolkit/components/antitracking/test/browser/3rdPartyUI.html new file mode 100644 index 0000000000..57693fbcbd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyUI.html @@ -0,0 +1,32 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Tracker</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWO.html b/toolkit/components/antitracking/test/browser/3rdPartyWO.html new file mode 100644 index 0000000000..7986b31063 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyWO.html @@ -0,0 +1,80 @@ +<html> +<head> + <title>Interact with me!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Interact with me!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.blockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, /* Phase */ 2).then(_ => { + info("Let's do a window.open()"); + return new Promise(resolve => { + if (location.search == "?noopener") { + let features = "noopener"; + + window.open("3rdPartyOpen.html", undefined, features); + setTimeout(resolve, 1000); + } else { + onmessage = resolve; + + window.open("3rdPartyOpen.html"); + } + }); + }).then(_ => { + info("The popup has been dismissed!"); + // First time storage access should not be granted because the tracker has + // not had user interaction yet. + let runnableStr = `(() => {return (${e.data.blockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + return runnable.call(this, /* Phase */ 2); + }).then(_ => { + info("Let's interact with the tracker"); + return new Promise(resolve => { + onmessage = resolve; + + window.open("3rdPartyOpenUI.html?messageme"); + }); + }).then(_ => { + info("Let's do another window.open()"); + return new Promise(resolve => { + if (location.search == "?noopener") { + let features = "noopener"; + + window.open("3rdPartyOpen.html", undefined, features); + setTimeout(resolve, 1000); + } else { + onmessage = resolve; + + window.open("3rdPartyOpen.html"); + } + }); + }).then(_ => { + // This time the tracker must have been able to obtain first-party storage + // access because it has had user interaction before. + let runnableStr = `(() => {return (${e.data.nonBlockingCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + return runnable.call(this, /* Phase */ 2); + }).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/3rdPartyWorker.html b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html new file mode 100644 index 0000000000..e79a992660 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/3rdPartyWorker.html @@ -0,0 +1,55 @@ +<html> +<head> + <title>Tracker</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Tracker</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function workerCode() { + onmessage = e => { + try { + indexedDB.open("test", "1"); + postMessage(true); + } catch (e) { + postMessage(false); + } + }; +} + +var worker; +function createWorker() { + let blob = new Blob([workerCode.toString() + "; workerCode();"]); + let blobURL = URL.createObjectURL(blob); + info("Blob created"); + + worker = new Worker(blobURL); + info("Worker created"); +} + +onmessage = function(e) { + let runnableStr = `(() => {return (${e.data.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + runnable.call(this, e.data.arg || /* Phase */ 3).then(_ => { + parent.postMessage({ type: "finish" }, "*"); + }); +}; + +createWorker(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/antitracking_head.js b/toolkit/components/antitracking/test/browser/antitracking_head.js new file mode 100644 index 0000000000..1b98e38f83 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/antitracking_head.js @@ -0,0 +1,1448 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from head.js */ + +"use strict"; + +var gFeatures = undefined; +var gTestTrackersCleanedUp = false; +var gTestTrackersCleanupRegistered = false; + +/** + * Force garbage collection. + */ +function forceGC() { + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); +} + +this.AntiTracking = { + runTestInNormalAndPrivateMode( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest = true, + userInteractionTest = true, + expectedBlockingNotifications = Ci.nsIWebProgressListener + .STATE_COOKIES_BLOCKED_TRACKER, + iframeSandbox = null, + accessRemoval = null, + callbackAfterRemoval = null + ) { + // Normal mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + false, + iframeSandbox, + accessRemoval, + callbackAfterRemoval + ); + + // Private mode + this.runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest, + userInteractionTest, + expectedBlockingNotifications, + true, + iframeSandbox, + accessRemoval, + callbackAfterRemoval + ); + }, + + runTest( + name, + callbackTracking, + callbackNonTracking, + cleanupFunction, + extraPrefs, + windowOpenTest = true, + userInteractionTest = true, + expectedBlockingNotifications = Ci.nsIWebProgressListener + .STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow = false, + iframeSandbox = null, + accessRemoval = null, + callbackAfterRemoval = null + ) { + let runExtraTests = true; + let options = {}; + if (typeof callbackNonTracking == "object" && !!callbackNonTracking) { + options.callback = callbackNonTracking.callback; + runExtraTests = callbackNonTracking.runExtraTests; + if ("cookieBehavior" in callbackNonTracking) { + options.cookieBehavior = callbackNonTracking.cookieBehavior; + } else { + options.cookieBehavior = BEHAVIOR_ACCEPT; + } + if ("expectedBlockingNotifications" in callbackNonTracking) { + options.expectedBlockingNotifications = + callbackNonTracking.expectedBlockingNotifications; + } else { + options.expectedBlockingNotifications = 0; + } + if ("blockingByAllowList" in callbackNonTracking) { + options.blockingByAllowList = callbackNonTracking.blockingByAllowList; + if (options.blockingByAllowList) { + // If we're on the allow list, there won't be any blocking! + options.expectedBlockingNotifications = 0; + } + } else { + options.blockingByAllowList = false; + } + callbackNonTracking = options.callback; + options.accessRemoval = null; + options.callbackAfterRemoval = null; + } + + // Here we want to test that a 3rd party context is simply blocked. + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: callbackTracking, + extraPrefs, + expectedBlockingNotifications, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + if (callbackNonTracking) { + // Phase 1: Here we want to test that a 3rd party context is not blocked if pref is off. + if (runExtraTests) { + // There are five ways in which the third-party context may not be blocked: + // * If the cookieBehavior pref causes it to not be blocked. + // * If the contentBlocking pref causes it to not be blocked. + // * If both of these prefs cause it to not be blocked. + // * If the top-level page is on the content blocking allow list. + // * If the contentBlocking third-party cookies UI pref is off, the allow list will be ignored. + // All of these cases are tested here. + this._createTask({ + name, + cookieBehavior: BEHAVIOR_ACCEPT, + allowList: false, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_ACCEPT, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: callbackTracking, + extraPrefs, + expectedBlockingNotifications: expectedBlockingNotifications + ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL + : 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name: name + " reject foreign without exception", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name: name + " reject foreign with exception", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: true, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: callbackTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: expectedBlockingNotifications + ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN + : 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: callbackNonTracking, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ], + expectedBlockingNotifications: false, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: true, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: 0, + runInPrivateWindow, + iframeSandbox, + accessRemoval, + callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + + this._createTask({ + name, + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: callbackNonTracking, + extraPrefs, + expectedBlockingNotifications: false, + runInPrivateWindow, + iframeSandbox, + accessRemoval: null, // only passed with non-blocking callback + callbackAfterRemoval: null, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }); + this._createCleanupTask(cleanupFunction); + } else { + // This is only used for imageCacheWorker.js tests + this._createTask({ + name, + cookieBehavior: options.cookieBehavior, + allowList: options.blockingByAllowList, + callback: options.callback, + extraPrefs, + expectedBlockingNotifications: options.expectedBlockingNotifications, + runInPrivateWindow, + iframeSandbox, + accessRemoval: options.accessRemoval, + callbackAfterRemoval: options.callbackAfterRemoval, + }); + this._createCleanupTask(cleanupFunction); + } + + // Phase 2: Here we want to test that a third-party context doesn't + // get blocked with when the same origin is opened through window.open(). + if (windowOpenTest) { + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createWindowOpenTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + } + + // Phase 3: Here we want to test that a third-party context doesn't + // get blocked with user interaction present + if (userInteractionTest) { + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + false, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + + // Now, check if it works for nested iframes. + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_TRACKER, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + extraPrefs + ); + this._createCleanupTask(cleanupFunction); + + this._createUserInteractionTask( + name, + BEHAVIOR_REJECT_FOREIGN, + callbackTracking, + callbackNonTracking, + runInPrivateWindow, + iframeSandbox, + true, + [ + ["network.cookie.rejectForeignWithExceptions.enabled", true], + ...(extraPrefs || []), + ] + ); + this._createCleanupTask(cleanupFunction); + } + } + }, + + _waitObserver(targetTopic, expectedCount) { + let cnt = 0; + + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject, topic, data) { + if (topic != targetTopic) { + return; + } + cnt++; + + if (cnt != expectedCount) { + return; + } + + Services.obs.removeObserver(observer, targetTopic); + resolve(); + }, targetTopic); + }); + }, + + _waitUserInteractionPerm() { + return this._waitObserver( + "antitracking-test-user-interaction-perm-added", + 1 + ); + }, + + _waitStorageAccessPerm(expectedCount) { + return this._waitObserver( + "antitracking-test-storage-access-perm-added", + expectedCount + ); + }, + + async interactWithTracker() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: TEST_3RD_PARTY_PAGE }, + async function (browser) { + info("Let's interact with the tracker"); + + await SpecialPowers.spawn(browser, [], async function () { + SpecialPowers.wrap(content.document).userInteractionForTesting(); + }); + } + ); + await BrowserTestUtils.closeWindow(win); + }, + + async _setupTest(win, cookieBehavior, runInPrivateWindow, extraPrefs) { + await SpecialPowers.flushPrefEnv(); + + await setCookieBehaviorPref(cookieBehavior, runInPrivateWindow); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["dom.security.https_first_pbm", false], + [ + "privacy.trackingprotection.annotate_channels", + cookieBehavior != BEHAVIOR_ACCEPT, + ], + ["privacy.restrict3rdpartystorage.console.lazy", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.antitracking.testing", true], + ], + }); + + if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) { + await SpecialPowers.pushPrefEnv({ set: extraPrefs }); + + let enableWebcompat = Services.prefs.getBoolPref( + "privacy.antitracking.enableWebcompat" + ); + + // If the skip list is disabled by pref, it will always return an empty + // list. + if (enableWebcompat) { + for (let item of extraPrefs) { + // When setting up exception URLs, we need to wait to ensure our prefs + // actually take effect. In order to do this, we set up a exception + // list observer and wait until it calls us back. + if (item[0] == "urlclassifier.trackingAnnotationSkipURLs") { + info("Waiting for the exception list service to initialize..."); + let classifier = Cc[ + "@mozilla.org/url-classifier/dbservice;1" + ].getService(Ci.nsIURIClassifier); + let feature = classifier.getFeatureByName("tracking-annotation"); + await TestUtils.waitForCondition(() => { + for (let x of item[1].toLowerCase().split(",")) { + if (feature.exceptionHostList.split(",").includes(x)) { + return true; + } + } + return false; + }, "Exception list service initialized"); + break; + } + } + } + } + + await UrlClassifierTestUtils.addTestTrackers(); + if (!gTestTrackersCleanupRegistered) { + registerCleanupFunction(_ => { + if (gTestTrackersCleanedUp) { + return; + } + UrlClassifierTestUtils.cleanupTestTrackers(); + gTestTrackersCleanedUp = true; + }); + gTestTrackersCleanupRegistered = true; + } + }, + + _createTask(options) { + add_task(async function () { + info( + "Starting " + + (options.cookieBehavior != BEHAVIOR_ACCEPT + ? "blocking" + : "non-blocking") + + " cookieBehavior (" + + options.cookieBehavior + + ") with" + + (options.allowList ? "" : "out") + + " allow list test " + + options.name + + " running in a " + + (options.runInPrivateWindow ? "private" : "normal") + + " window " + + " with iframe sandbox set to " + + options.iframeSandbox + + " and access removal set to " + + options.accessRemoval + + (typeof options.thirdPartyPage == "string" + ? " and third party page set to " + options.thirdPartyPage + : "") + + (typeof options.topPage == "string" + ? " and top page set to " + options.topPage + : "") + ); + + is( + !!options.callbackAfterRemoval, + !!options.accessRemoval, + "callbackAfterRemoval must be passed when accessRemoval is non-null" + ); + + let win = window; + if (options.runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + options.cookieBehavior, + options.runInPrivateWindow, + options.extraPrefs + ); + + let topPage; + if (typeof options.topPage == "string") { + topPage = options.topPage; + } else { + topPage = TEST_TOP_PAGE; + } + + let thirdPartyPage, thirdPartyDomainURI; + if (typeof options.thirdPartyPage == "string") { + thirdPartyPage = options.thirdPartyPage; + let url = new URL(thirdPartyPage); + thirdPartyDomainURI = Services.io.newURI(url.origin); + } else { + thirdPartyPage = TEST_3RD_PARTY_PAGE; + thirdPartyDomainURI = Services.io.newURI(TEST_3RD_PARTY_DOMAIN); + } + + // It's possible that the third-party domain has been exceptionlisted + // through extraPrefs, so let's try annotating it here and adjust our + // blocking expectations as necessary. + if ( + options.expectedBlockingNotifications == + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER + ) { + if ( + !(await AntiTracking._isThirdPartyPageClassifiedAsTracker( + topPage, + thirdPartyDomainURI + )) + ) { + options.expectedBlockingNotifications = 0; + } + } + + let cookieBlocked = 0; + let { expectedBlockingNotifications } = options; + if (!Array.isArray(expectedBlockingNotifications)) { + expectedBlockingNotifications = [expectedBlockingNotifications]; + } + let listener = { + onContentBlockingEvent(webProgress, request, event) { + for (const notification of expectedBlockingNotifications) { + if (event & notification) { + ++cookieBlocked; + } + } + }, + }; + function prepareTestEnvironmentOnPage() { + win.gBrowser.addProgressListener(listener); + + Services.console.reset(); + } + + if (!options.allowList) { + prepareTestEnvironmentOnPage(); + } + + let consoleWarningPromise; + + if (options.expectedBlockingNotifications) { + consoleWarningPromise = new Promise(resolve => { + let consoleListener = { + observe(msg) { + if ( + msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookieBlocked") + ) { + Services.console.unregisterListener(consoleListener); + resolve(); + } + }, + }; + + Services.console.registerListener(consoleListener); + }); + } else { + consoleWarningPromise = Promise.resolve(); + } + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, topPage); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Check the cookieJarSettings of the browser object"); + ok( + browser.cookieJarSettings, + "The browser object has the cookieJarSettings." + ); + is( + browser.cookieJarSettings.cookieBehavior, + options.cookieBehavior, + "The cookieJarSettings has the correct cookieBehavior" + ); + + if (options.allowList) { + info("Disabling content blocking for this page"); + win.gProtectionsHandler.disableForCurrentPage(); + + prepareTestEnvironmentOnPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + } + + info("Creating a 3rd party content"); + let doAccessRemovalChecks = + typeof options.accessRemoval == "string" && + options.cookieBehavior == BEHAVIOR_REJECT_TRACKER && + !options.allowList; + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + nextPage: TEST_4TH_PARTY_PAGE, + callback: options.callback.toString(), + callbackAfterRemoval: options.callbackAfterRemoval + ? options.callbackAfterRemoval.toString() + : null, + accessRemoval: options.accessRemoval, + iframeSandbox: options.iframeSandbox, + allowList: options.allowList, + doAccessRemovalChecks, + }, + ], + async function (obj) { + let id = "id" + Math.random(); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.id = id; + ifr.onload = function () { + info("Sending code to the 3rd party content"); + let callback = obj.allowList + "!!!" + obj.callback; + ifr.contentWindow.postMessage(callback, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + if (obj.doAccessRemovalChecks) { + info(`Running after removal checks (${obj.accessRemoval})`); + switch (obj.accessRemoval) { + case "navigate-subframe": + await new content.Promise(resolve => { + let ifr = content.document.getElementById(id); + let oldWindow = ifr.contentWindow; + ifr.onload = function () { + info("Sending code to the old 3rd party content"); + oldWindow.postMessage(obj.callbackAfterRemoval, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + ifr.src = obj.nextPage; + }); + break; + case "navigate-topframe": + // pass-through + break; + default: + ok( + false, + "Unexpected accessRemoval code passed: " + obj.accessRemoval + ); + break; + } + } + } + ); + + if ( + doAccessRemovalChecks && + options.accessRemoval == "navigate-topframe" + ) { + BrowserTestUtils.loadURIString(browser, TEST_4TH_PARTY_PAGE); + await BrowserTestUtils.browserLoaded(browser); + + let pageshow = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + gBrowser.goBack(); + await pageshow; + + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + callbackAfterRemoval: options.callbackAfterRemoval + ? options.callbackAfterRemoval.toString() + : null, + iframeSandbox: options.iframeSandbox, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info( + "Sending code to the 3rd party content to verify accessRemoval" + ); + ifr.contentWindow.postMessage(obj.callbackAfterRemoval, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + } + ); + } + + // Wait until the message appears on the console. + await consoleWarningPromise; + + let allMessages = Services.console.getMessageArray().filter(msg => { + try { + // Select all messages that the anti-tracking backend could generate. + return msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookieBlocked"); + } catch (e) { + return false; + } + }); + // When changing this list, please make sure to update the corresponding + // code in ReportBlockingToConsole(). + let expectedCategories = []; + let rawExpectedCategories = options.expectedBlockingNotifications; + if (!Array.isArray(rawExpectedCategories)) { + // if given a single value to match, expect each message to match it + rawExpectedCategories = Array(allMessages.length).fill( + rawExpectedCategories + ); + } + for (let category of rawExpectedCategories) { + switch (category) { + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION: + expectedCategories.push("cookieBlockedPermission"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER: + expectedCategories.push("cookieBlockedTracker"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL: + expectedCategories.push("cookieBlockedAll"); + break; + case Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN: + expectedCategories.push("cookieBlockedForeign"); + break; + } + } + + if (!expectedCategories.length) { + is(allMessages.length, 0, "No console messages should be generated"); + } else { + ok(!!allMessages.length, "Some console message should be generated"); + if (options.errorMessageDomains) { + is( + allMessages.length, + options.errorMessageDomains.length, + "Enough items provided in errorMessageDomains" + ); + } + } + let index = 0; + for (let msg of allMessages) { + is( + msg.category, + expectedCategories[index], + `Message ${index} should be of expected category` + ); + + if (options.errorMessageDomains) { + ok( + msg.errorMessage.includes(options.errorMessageDomains[index]), + `Error message domain ${options.errorMessageDomains[index]} (${index}) found in "${msg.errorMessage}"` + ); + index++; + } + } + + if (options.allowList) { + info("Enabling content blocking for this page"); + win.gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + } + + win.gBrowser.removeProgressListener(listener); + + if (!!cookieBlocked != !!options.expectedBlockingNotifications) { + ok(false, JSON.stringify(cookieBlocked)); + ok(false, JSON.stringify(options.expectedBlockingNotifications)); + } + is( + !!cookieBlocked, + !!options.expectedBlockingNotifications, + "Checking cookie blocking notifications" + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (options.runInPrivateWindow) { + win.close(); + } + }); + }, + + _createCleanupTask(cleanupFunction) { + add_task(async function () { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + forceGC(); + }); + }, + + _createWindowOpenTask( + name, + cookieBehavior, + blockingCallback, + nonBlockingCallback, + runInPrivateWindow, + iframeSandbox, + testInSubIFrame, + extraPrefs + ) { + add_task(async function () { + info( + `Starting window-open${ + testInSubIFrame ? " sub iframe" : "" + } test ${name}` + ); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + cookieBehavior, + runInPrivateWindow, + extraPrefs + ); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Create a first-level iframe to test sub iframes."); + if (testInSubIFrame) { + let iframeBrowsingContext = await SpecialPowers.spawn( + browser, + [{ page: TEST_IFRAME_PAGE }], + async function (obj) { + // Add an iframe. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + return ifr.browsingContext; + } + ); + + browser = iframeBrowsingContext; + } + + let pageURL = TEST_3RD_PARTY_PAGE_WO; + if (gFeatures == "noopener") { + pageURL += "?noopener"; + } + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: pageURL, + blockingCallback: blockingCallback.toString(), + nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + }, + + _createUserInteractionTask( + name, + cookieBehavior, + blockingCallback, + nonBlockingCallback, + runInPrivateWindow, + iframeSandbox, + testInSubIFrame, + extraPrefs + ) { + add_task(async function () { + info( + `Starting user-interaction${ + testInSubIFrame ? " sub iframe" : "" + } test ${name}` + ); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + await AntiTracking._setupTest( + win, + cookieBehavior, + runInPrivateWindow, + extraPrefs + ); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + if (testInSubIFrame) { + info("Create a first-level iframe to test sub iframes."); + let iframeBrowsingContext = await SpecialPowers.spawn( + browser, + [{ page: TEST_IFRAME_PAGE }], + async function (obj) { + // Add an iframe. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + return ifr.browsingContext; + } + ); + + browser = iframeBrowsingContext; + } + + // The following test will open an popup which interacts with the tracker + // page. So there will be an user-interaction permission added. We wait + // it explicitly. + let promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + blockingCallback: blockingCallback.toString(), + iframeSandbox, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.blockingCallback }, + "*" + ); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + SpecialPowers.spawn( + ifr, + [{ popup: obj.popup }], + async function (obj) { + content.open(obj.popup); + } + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info( + "First time, the 3rd party content should not have access to first party storage " + + "because the tracker did not have user interaction" + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.blockingCallback }, + "*" + ); + }); + } + ); + + // We wait until the user-interaction permission is added. + await promiseUIPerm; + + // We also need to wait the user-interaction permission here. + promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + await AntiTracking.interactWithTracker(); + await promiseUIPerm; + + // Following test will also open an popup to interact with the page. We + // need to explicitly wait it. Without waiting it, it could be added after + // we clear up the test and interfere the next test. + promiseUIPerm = AntiTracking._waitUserInteractionPerm(); + + // We have to wait until the storage access permission is added. This has + // the same reason as above user-interaction permission. Note that there + // will be two storage access permission added due to the way how we + // trigger the heuristic. The first permission is added due to 'Opener' + // heuristic and the second one is due to 'Opener after user interaction'. + // The page we use to trigger the heuristic will trigger both heuristic, + // so we have to wait 2 permissions. + let promiseStorageAccessPerm = AntiTracking._waitStorageAccessPerm(2); + + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + nonBlockingCallback: nonBlockingCallback.toString(), + iframeSandbox, + }, + ], + async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + if (typeof obj.iframeSandbox == "string") { + ifr.setAttribute("sandbox", obj.iframeSandbox); + } + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + SpecialPowers.spawn( + ifr, + [{ popup: obj.popup }], + async function (obj) { + content.open(obj.popup); + } + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info( + "The 3rd party content should now have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: obj.nonBlockingCallback }, + "*" + ); + }); + } + ); + + // Explicitly wait the user-interaction and storage access permission + // before we do the cleanup. + await promiseUIPerm; + await promiseStorageAccessPerm; + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + }, + + async _isThirdPartyPageClassifiedAsTracker(topPage, thirdPartyDomainURI) { + let channel; + await new Promise((resolve, reject) => { + channel = NetUtil.newChannel({ + uri: thirdPartyDomainURI, + loadingPrincipal: Services.scriptSecurityManager.createContentPrincipal( + thirdPartyDomainURI, + {} + ), + securityFlags: + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + channel + .QueryInterface(Ci.nsIHttpChannelInternal) + .setTopWindowURIIfUnknown(Services.io.newURI(topPage)); + + function Listener() {} + Listener.prototype = { + onStartRequest(request) {}, + onDataAvailable(request, stream, off, cnt) { + // Consume the data to prevent hitting the assertion. + NetUtil.readInputStreamToString(stream, cnt); + }, + onStopRequest(request, st) { + let status = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; + if (status == 200) { + resolve(); + } else { + reject(); + } + }, + }; + let listener = new Listener(); + channel.asyncOpen(listener); + }); + + return !!( + channel.QueryInterface(Ci.nsIClassifiedChannel).classificationFlags & + Ci.nsIClassifiedChannel.CLASSIFIED_ANY_BASIC_TRACKING + ); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/browser-blocking.ini b/toolkit/components/antitracking/test/browser/browser-blocking.ini new file mode 100644 index 0000000000..1533d1273f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser-blocking.ini @@ -0,0 +1,72 @@ +[DEFAULT] +skip-if = os == "linux" && (asan || tsan) # bug 1662229 - task exception +prefs = + # Disable the Storage Access API prompts for all of the tests in this directory + dom.storage_access.prompt.testing=true + dom.storage_access.prompt.testing.allow=true + dom.testing.sync-content-blocking-notifications=true + # Enable the window.open() heuristics globally in this directory + privacy.restrict3rdpartystorage.heuristic.window_open=true + privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true + # Disable https-first because of explicit http/https testing + dom.security.https_first=false + +support-files = + head.js + antitracking_head.js + iframe.html + image.sjs + page.html + 3rdParty.html + 3rdPartyRelay.html + 3rdPartySVG.html + 3rdPartyUI.html + 3rdPartyWO.html + 3rdPartyWorker.html + 3rdPartyOpen.html + 3rdPartyOpenUI.html + empty.js + popup.html + server.sjs + storageAccessAPIHelpers.js + 3rdPartyStorage.html + 3rdPartyStorageWO.html + 3rdPartyPartitioned.html + localStorage.html + !/browser/modules/test/browser/head.js + !/browser/base/content/test/general/head.js + !/browser/base/content/test/protectionsUI/cookieServer.sjs + !/browser/base/content/test/protectionsUI/trackingPage.html + !/browser/base/content/test/protectionsUI/trackingAPI.js + +[browser_blockingCookies.js] +skip-if = socketprocess_networking +[browser_blockingDOMCacheSAA.js] +skip-if = socketprocess_networking +[browser_blockingDOMCacheAlwaysPartition.js] +skip-if = socketprocess_networking +[browser_blockingDOMCacheAlwaysPartitionSAA.js] +skip-if = socketprocess_networking +[browser_blockingDOMCache.js] +[browser_blockingIndexedDb.js] +skip-if = os == 'linux' && socketprocess_networking +[browser_blockingIndexedDbInWorkers.js] +skip-if = os == 'linux' && socketprocess_networking +[browser_blockingIndexedDbInWorkers2.js] +[browser_blockingLocalStorage.js] +skip-if = os == 'linux' && socketprocess_networking +[browser_blockingSessionStorage.js] +[browser_blockingServiceWorkers.js] +[browser_blockingServiceWorkersStorageAccessAPI.js] +[browser_blockingSharedWorkers.js] +skip-if = os == 'linux' && socketprocess_networking +[browser_blockingMessaging.js] +skip-if = os == "linux" && debug #bug 1627094 +[browser_blockingNoOpener.js] +[browser_contentBlockingAllowListPrincipal.js] +support-files = + sandboxed.html + sandboxed.html^headers^ +[browser_contentBlockingTelemetry.js] +skip-if = + win10_2004 && fission && !debug # high frequency intermittent, same as bug 1727097 diff --git a/toolkit/components/antitracking/test/browser/browser.ini b/toolkit/components/antitracking/test/browser/browser.ini new file mode 100644 index 0000000000..52b25a4b60 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser.ini @@ -0,0 +1,226 @@ +[DEFAULT] +skip-if = + os == "linux" && (asan || tsan) # bug 1662229 - task exception + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +prefs = + # Disable the Storage Access API prompts for all of the tests in this directory + dom.storage_access.prompt.testing=true + dom.storage_access.prompt.testing.allow=true + dom.testing.sync-content-blocking-notifications=true + # Enable the window.open() heuristics globally in this directory + privacy.restrict3rdpartystorage.heuristic.window_open=true + privacy.restrict3rdpartystorage.heuristic.opened_window_after_interaction=true + # Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + network.cookie.sameSite.laxByDefault=false + # Disable https-first because of explicit http/https testing + dom.security.https_first=false + +support-files = + container.html + container2.html + embedder.html + embedder2.html + head.js + antitracking_head.js + dynamicfpi_head.js + partitionedstorage_head.js + storage_access_head.js + cookiesCORS.sjs + iframe.html + image.sjs + imageCacheWorker.js + page.html + 3rdParty.html + 3rdPartyRelay.html + 3rdPartySVG.html + 3rdPartyUI.html + 3rdPartyWO.html + 3rdPartyWorker.html + 3rdPartyOpen.html + 3rdPartyOpenUI.html + empty.js + empty-altsvc.js + empty-altsvc.js^headers^ + empty.html + file_iframe_document_open.html + file_localStorage.html + popup.html + redirect.sjs + server.sjs + storageAccessAPIHelpers.js + 3rdPartyStorage.html + 3rdPartyStorageWO.html + 3rdPartyPartitioned.html + localStorage.html + raptor.jpg + !/browser/modules/test/browser/head.js + !/browser/base/content/test/general/head.js + !/browser/base/content/test/protectionsUI/cookieServer.sjs + !/browser/base/content/test/protectionsUI/trackingPage.html + !/browser/base/content/test/protectionsUI/trackingAPI.js + !/toolkit/content/tests/browser/common/mockTransfer.js + +[browser_aboutblank.js] +[browser_allowListNotifications.js] +[browser_allowListNotifications_alwaysPartition.js] +support-files = subResources.sjs +[browser_addonHostPermissionIgnoredInTP.js] +[browser_allowListSeparationInPrivateAndNormalWindows.js] +skip-if = os == "mac" && !debug # Bug 1503778, 1577362 +[browser_backgroundImageAssertion.js] +[browser_doublyNestedTracker.js] +[browser_emailtracking.js] +[browser_existingCookiesForSubresources.js] +[browser_fileUrl.js] +[browser_firstPartyCookieRejectionHonoursAllowList.js] +[browser_fpiServiceWorkers_fingerprinting.js] +[browser_hasStorageAccess.js] +[browser_hasStorageAccess_alwaysPartition.js] +[browser_iframe_document_open.js] +[browser_imageCache4.js] +[browser_imageCache8.js] +[browser_onBeforeRequestNotificationForTrackingResources.js] +[browser_onModifyRequestNotificationForTrackingResources.js] +[browser_permissionInNormalWindows.js] +[browser_permissionInNormalWindows_alwaysPartition.js] +[browser_permissionInPrivateWindows.js] +[browser_permissionInPrivateWindows_alwaysPartition.js] +[browser_permissionPropagation.js] +skip-if = + os == "linux" && bits == 64 # Bug 1645505 + os == "win" && debug # Bug 1645505 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_referrerDefaultPolicy.js] +support-files = referrer.sjs +[browser_siteSpecificWorkArounds.js] +[browser_subResources.js] +support-files = subResources.sjs +[browser_subResourcesPartitioned.js] +support-files = subResources.sjs +[browser_subResourcesPartitioned_alwaysPartition.js] +support-files = subResources.sjs +[browser_script.js] +support-files = tracker.js +[browser_userInteraction.js] +[browser_serviceWorkersWithStorageAccessGranted.js] +[browser_storageAccess_TopLevel_Arguments.js] +[browser_storageAccess_TopLevel_CookieBehavior.js] +[browser_storageAccess_TopLevel_CookiePermission.js] +[browser_storageAccess_TopLevel_CrossOriginSameSite.js] +[browser_storageAccess_TopLevel_Doorhanger.js] +[browser_storageAccess_TopLevel_Embed.js] +[browser_storageAccess_TopLevel_Enable.js] +[browser_storageAccess_TopLevel_RequireIntermediatePermission.js] +[browser_storageAccess_TopLevel_StorageAccessPermission.js] +[browser_storageAccess_TopLevel_UserActivation.js] +skip-if = debug # Bug 1700551 +[browser_storageAccessAutograntRequiresUserInteraction.js] +[browser_storageAccessDeniedGivesNoUserInteraction.js] +[browser_storageAccessDoorHanger.js] +[browser_storageAccessFrameInteractionGrantsUserInteraction.js] +[browser_storageAccessGrantedGivesUserInteraction.js] +[browser_storageAccessPrivilegeAPI.js] +[browser_storageAccessPromiseRejectHandlerUserInteraction.js] +[browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js] +[browser_storageAccessPromiseResolveHandlerUserInteraction.js] +[browser_storageAccessRemovalNavigateSubframe.js] +[browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js] +[browser_storageAccessRemovalNavigateTopframe.js] +[browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js] +[browser_storageAccessSandboxed.js] +[browser_storageAccessSandboxed_alwaysPartition.js] +[browser_storageAccessScopeDifferentSite.js] +[browser_storageAccessScopeSameOrigin.js] +[browser_storageAccessScopeSameSiteRead.js] +[browser_storageAccessScopeSameSiteWrite.js] +[browser_storageAccessThirdPartyChecks.js] +[browser_storageAccessThirdPartyChecks_alwaysPartition.js] +[browser_storageAccessWithDynamicFpi.js] +[browser_storageAccessWithHeuristics.js] +[browser_allowPermissionForTracker.js] +[browser_denyPermissionForTracker.js] +[browser_localStorageEvents.js] +[browser_partitionedLocalStorage.js] +[browser_partitionedLocalStorage_events.js] +support-files = localStorageEvents.html +[browser_workerPropagation.js] +support-files = workerIframe.html +[browser_cookieBetweenTabs.js] +[browser_partitionedMessaging.js] +skip-if = true #Bug 1588241 +[browser_partitionedIndexedDB.js] +[browser_partitionedConsoleMessage.js] +[browser_partitionedCookies.js] +support-files = cookies.sjs +[browser_partitionedDOMCache.js] +[browser_partitionedServiceWorkers.js] +support-files = dedicatedWorker.js matchAll.js serviceWorker.js +[browser_partitionedSharedWorkers.js] +support-files = sharedWorker.js partitionedSharedWorker.js +[browser_PBMCookieBehavior.js] +[browser_socialtracking.js] +[browser_socialtracking_save_image.js] +[browser_thirdPartyStorageRejectionForCORS.js] +[browser_urlDecorationStripping.js] +[browser_urlDecorationStripping_alwaysPartition.js] +tags = remote-settings +[browser_urlQueryStringStripping_allowList.js] +support-files = file_stripping.html +[browser_urlQueryStringStripping_telemetry.js] +support-files = file_stripping.html +[browser_urlQueryStringStripping_telemetry_2.js] +support-files = file_stripping.html +[browser_urlQueryStringStripping.js] +skip-if = + fission && os == "linux" && asan # Bug 1713909 - new Fission platform triage +support-files = file_stripping.html +[browser_urlQueryStringStripping_pbmode.js] +support-files = file_stripping.html +[browser_urlQueryStringStripping_nimbus.js] +support-files = file_stripping.html +[browser_urlQueryStrippingListService.js] +[browser_staticPartition_cache.js] +support-files = + !/browser/components/originattributes/test/browser/file_cache.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.audio.ogg + !/browser/components/originattributes/test/browser/file_thirdPartyChild.embed.png + !/browser/components/originattributes/test/browser/file_thirdPartyChild.fetch.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.iframe.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.img.png + !/browser/components/originattributes/test/browser/file_thirdPartyChild.favicon.png + !/browser/components/originattributes/test/browser/file_thirdPartyChild.import.js + !/browser/components/originattributes/test/browser/file_thirdPartyChild.link.css + !/browser/components/originattributes/test/browser/file_thirdPartyChild.object.png + !/browser/components/originattributes/test/browser/file_thirdPartyChild.request.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.script.js + !/browser/components/originattributes/test/browser/file_thirdPartyChild.sharedworker.js + !/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv + !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.fetch.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.js + !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.request.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.worker.xhr.html + !/browser/components/originattributes/test/browser/file_thirdPartyChild.xhr.html +[browser_staticPartition_network.js] +[browser_staticPartition_CORS_preflight.js] +support-files = browser_staticPartition_CORS_preflight.sjs +[browser_staticPartition_HSTS.js] +support-files = browser_staticPartition_HSTS.sjs +[browser_staticPartition_saveAs.js] +skip-if = + os == "linux" && bits == 64 # Bug 1775746 + win10_2004 && debug # Bug 1775746 +support-files = + file_saveAsImage.sjs + file_saveAsVideo.sjs + file_saveAsPageInfo.html + file_video.ogv +[browser_staticPartition_tls_session.js] +[browser_staticPartition_websocket.js] +skip-if = + os == 'mac' && verify # Bug 1721210 +support-files = + file_ws_handshake_delay_wsh.py +[browser_partitionedClearSiteDataHeader.js] +support-files = + clearSiteData.sjs +[browser_AntiTrackingETPHeuristic.js] diff --git a/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js new file mode 100644 index 0000000000..f5274d8ba9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_AntiTrackingETPHeuristic.js @@ -0,0 +1,261 @@ +"use strict"; + +const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "page.html"; +const TEST_REDIRECT_PAGE = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; +const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html"; +const TEST_TRACKING_REDIRECT_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs"; +const TEST_ANOTHER_TRACKING_REDIRECT_PAGE = + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "redirect.sjs"; + +const TEST_CASES = [ + // Tracker(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(No interaction) -> Non-Tracker + { + trackersHasUserInteraction: [], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + }, + // Non-Tracker -> Tracker(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_REDIRECT_PAGE, TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(Interacted) -> Tracker(Interacted) -> Tracker(Interacted) + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_TRACKING_REDIRECT_PAGE, + TEST_TRACKING_PAGE, + ], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_3RD_PARTY_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Tracker2(Interacted) -> Non-Tracker + { + trackersHasUserInteraction: [ + TEST_3RD_PARTY_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + ], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Non-Tracker -> Tracker2(No interaction) -> Non-Tracker + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker1(Interacted) -> Non-Tracker -> Tracker2(Interacted) -> Non-Tracker + // Note that the result is not quite correct in this case. We are supposed to + // grant access to the least tracker instead of the first one. But, this is + // the behavior how we act so far. We would fix this in another bug. + { + trackersHasUserInteraction: [ + TEST_3RD_PARTY_DOMAIN, + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS, + ], + redirects: [ + TEST_TRACKING_REDIRECT_PAGE, + TEST_REDIRECT_PAGE, + TEST_ANOTHER_TRACKING_REDIRECT_PAGE, + TEST_PAGE, + ], + expectedPermissionNumber: 1, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.ALLOW_ACTION, + ], + ], + }, + // Tracker(Interacted) -> Non-Tracker (heuristic disabled) + { + trackersHasUserInteraction: [TEST_3RD_PARTY_DOMAIN], + redirects: [TEST_TRACKING_REDIRECT_PAGE, TEST_PAGE], + expectedPermissionNumber: 0, + expects: [ + [ + TEST_DOMAIN, + TEST_3RD_PARTY_DOMAIN, + Ci.nsIPermissionManager.UNKNOWN_ACTION, + ], + ], + extraPrefs: [["privacy.antitracking.enableWebcompat", false]], + }, +]; + +async function interactWithSpecificTracker(aTracker) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: aTracker }, + async function (browser) { + info("Let's interact with the tracker"); + + await SpecialPowers.spawn(browser, [], async function () { + SpecialPowers.wrap(content.document).userInteractionForTesting(); + }); + } + ); + await BrowserTestUtils.closeWindow(win); +} + +function getNumberOfStorageAccessPermissions() { + let num = 0; + for (let perm of Services.perms.all) { + if (perm.type.startsWith("3rdPartyStorage^")) { + num++; + } + } + return num; +} + +async function verifyStorageAccessPermission(aExpects) { + for (let expect of aExpects) { + let uri = Services.io.newURI(expect[0]); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let access = Services.perms.testPermissionFromPrincipal( + principal, + `3rdPartyStorage^${expect[1].slice(0, -1)}` + ); + + is(access, expect[2], "The storage access is set correctly"); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +add_task(async function testETPRedirectHeuristic() { + info("Starting testing ETP redirect heuristic ..."); + + for (const test of TEST_CASES) { + let { extraPrefs } = test; + if (extraPrefs) { + await SpecialPowers.pushPrefEnv({ + set: extraPrefs, + }); + } + + // First, clear all permissions. + Services.perms.removeAll(); + + for (const tracker of test.trackersHasUserInteraction) { + info(`Interact with ${tracker} in top-level.`); + await interactWithSpecificTracker(tracker); + } + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking page and trigger the top-level redirect."); + SpecialPowers.spawn(browser, [test.redirects], async redirects => { + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = redirects.shift() + "?" + redirects.join("|"); + content.document.body.appendChild(link); + link.click(); + }); + + let finalRedirectDest = test.redirects[test.redirects.length - 1]; + + await BrowserTestUtils.browserLoaded(browser, false, finalRedirectDest); + + is( + getNumberOfStorageAccessPermissions(), + test.expectedPermissionNumber, + "The number of storage permissions is correct." + ); + + await verifyStorageAccessPermission(test.expects); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (extraPrefs) { + await SpecialPowers.popPrefEnv(); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js new file mode 100644 index 0000000000..ebed00f23c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_PBMCookieBehavior.js @@ -0,0 +1,108 @@ +"use strict"; + +// This test will run all combinations of CookieBehavior. So, request a longer +// timeout here +requestLongerTimeout(3); + +const COOKIE_BEHAVIORS = [ + Ci.nsICookieService.BEHAVIOR_ACCEPT, + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +]; + +async function verifyCookieBehavior(browser, expected) { + await SpecialPowers.spawn( + browser, + [{ expected, page: TEST_3RD_PARTY_PAGE }], + async obj => { + is( + content.document.cookieJarSettings.cookieBehavior, + obj.expected, + "The tab in the window has the expected CookieBehavior." + ); + + // Create an 3rd party iframe and check the cookieBehavior. + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + await SpecialPowers.spawn( + ifr.browsingContext, + [obj.expected], + async expected => { + is( + content.document.cookieJarSettings.cookieBehavior, + expected, + "The iframe in the window has the expected CookieBehavior." + ); + } + ); + } + ); +} + +add_task(async function () { + for (let regularCookieBehavior of COOKIE_BEHAVIORS) { + for (let PBMCookieBehavior of COOKIE_BEHAVIORS) { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", regularCookieBehavior], + ["network.cookie.cookieBehavior.pbmode", PBMCookieBehavior], + ["dom.security.https_first_pbm", false], + ], + }); + + info( + ` Start testing with regular cookieBehavior(${regularCookieBehavior}) and PBM cookieBehavior(${PBMCookieBehavior})` + ); + + info(" Open a tab in regular window."); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info( + " Verify if the tab in regular window has the expected cookieBehavior." + ); + await verifyCookieBehavior(tab.linkedBrowser, regularCookieBehavior); + BrowserTestUtils.removeTab(tab); + + info(" Open a tab in private window."); + let pb_win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + tab = await BrowserTestUtils.openNewForegroundTab( + pb_win.gBrowser, + TEST_TOP_PAGE + ); + + let expectPBMCookieBehavior = PBMCookieBehavior; + + // The private cookieBehavior will mirror the regular pref if the regular + // pref has a user value and the private pref doesn't have a user pref. + if ( + Services.prefs.prefHasUserValue("network.cookie.cookieBehavior") && + !Services.prefs.prefHasUserValue("network.cookie.cookieBehavior.pbmode") + ) { + expectPBMCookieBehavior = regularCookieBehavior; + } + + info( + " Verify if the tab in private window has the expected cookieBehavior." + ); + await verifyCookieBehavior(tab.linkedBrowser, expectPBMCookieBehavior); + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(pb_win); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_aboutblank.js b/toolkit/components/antitracking/test/browser/browser_aboutblank.js new file mode 100644 index 0000000000..f80a948771 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_aboutblank.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_aboutblankInIframe() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async function (obj) { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + ifr.src = "about:blank"; + content.document.body.appendChild(ifr); + await loading; + + await SpecialPowers.spawn(ifr, [], async function (obj) { + ok( + content.navigator.cookieEnabled, + "Cookie should be enabled in about blank" + ); + }); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js new file mode 100644 index 0000000000..44664e239a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_addonHostPermissionIgnoredInTP.js @@ -0,0 +1,46 @@ +add_task(async function () { + info("Starting test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.trackingprotection.enabled", true]], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["https://tracking.example.com/"] }, + files: { + "page.html": + '<html><head></head><body><script src="script.js"></script><iframe src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/container2.html"></iframe></body></html>', + "script.js": + 'window.count=0;window.p=new Promise(resolve=>{onmessage=e=>{count=e.data.data;resolve();};});p.then(()=>{document.documentElement.setAttribute("count",count);});', + }, + async background() { + browser.test.sendMessage("ready", browser.runtime.getURL("page.html")); + }, + }); + await extension.startup(); + let url = await extension.awaitMessage("ready"); + + info("Creating a new tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = tab.linkedBrowser; + + info("Verify the number of script nodes found"); + await ContentTask.spawn(browser, [], async function (obj) { + // Need to wait a bit for cross-process postMessage... + await ContentTaskUtils.waitForCondition( + () => content.document.documentElement.getAttribute("count") !== null, + "waiting for 'count' attribute" + ); + let count = content.document.documentElement.getAttribute("count"); + is(count, 3, "Expected script nodes found"); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + await extension.unload(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js new file mode 100644 index 0000000000..999c6f93a0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications.js @@ -0,0 +1,141 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + // Now load subresources from a few third-party origins. + // We should expect to see none of these origins in the content blocking log at the end. + await fetch( + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + let expectTrackerFound = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + is(originLog.length, 1, "We should have 1 entry in the compressed log"); + expectTrackerFound(originLog[0]); + } + + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js new file mode 100644 index 0000000000..0fd677e27f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListNotifications_alwaysPartition.js @@ -0,0 +1,142 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + // Now load subresources from a few third-party origins. + // We should expect to see none of these origins in the content blocking log at the end. + await fetch( + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://test2.example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + const nsIWPL = Ci.nsIWebProgressListener; + let expectTrackerFound = (item, expect) => { + is(item[0], expect, "Correct blocking type reported"); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + + is(originLog.length, 1, "We should have one entry in the compressed log"); + expectTrackerFound( + originLog[0], + nsIWPL.STATE_LOADED_LEVEL_1_TRACKING_CONTENT + ); + } + + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js new file mode 100644 index 0000000000..38eb9fc090 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowListSeparationInPrivateAndNormalWindows.js @@ -0,0 +1,52 @@ +// This test works by setting up an exception for the private window allow list +// manually, and it then expects to see some blocking notifications (note the +// document.cookie setter in the blocking callback.) +// If the exception lists aren't handled separately, we'd get confused and put +// the pages loaded under this test in the allow list, which would result in +// the test not passing because no blocking notifications would be observed. + +// Testing the reverse case would also be interesting, but unfortunately there +// isn't a super easy way to do that with our antitracking test framework since +// private windows wouldn't send any blocking notifications as they don't have +// storage access in the first place. + +"use strict"; +add_task(async _ => { + let uri = Services.io.newURI("https://example.net"); + PermissionTestUtils.add( + uri, + "trackingprotection-pb", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking.runTest( + "Test that we don't honour a private allow list exception in a normal window", + // Blocking callback + async _ => { + document.cookie = "name=value"; + }, + + // Non blocking callback + async _ => { + // Nothing to do here. + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, // no extra prefs + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window diff --git a/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js new file mode 100644 index 0000000000..e628199ead --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_allowPermissionForTracker.js @@ -0,0 +1,64 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "https://tracking.example.org", + "cookie", + Services.perms.ALLOW_ACTION + ); + PermissionTestUtils.add( + "https://tracking.example.com", + "cookie", + Services.perms.ALLOW_ACTION + ); + // Grant interaction permission so we can directly call + // requestStorageAccess from the tracker. + PermissionTestUtils.add( + "https://tracking.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do honour a cookie permission for nested windows", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: false, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + ok(document.cookie != "", "Nothing is blocked"); + + // requestStorageAccess should resolve + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document + .requestStorageAccess() + .then(() => { + ok(true, "Should grant storage access"); + }) + .catch(() => { + ok(false, "Should grant storage access"); + }); + SpecialPowers.wrap(document).clearUserGestureActivation(); + }, + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + extraPrefs: [["network.cookie.sameSite.laxByDefault", false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js new file mode 100644 index 0000000000..16eec0da9e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_backgroundImageAssertion.js @@ -0,0 +1,69 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ page: TEST_3RD_PARTY_PAGE_WITH_SVG }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + ok(true, "No crash, hopefully!"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingCookies.js b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js new file mode 100644 index 0000000000..a8e69b5e15 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingCookies.js @@ -0,0 +1,183 @@ +requestLongerTimeout(4); + +// Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +AntiTracking.runTestInNormalAndPrivateMode( + "Set/Get Cookies", + // Blocking callback + async _ => { + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + for (let arg of ["?checkonly", "?redirect-checkonly"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + is(document.cookie, "", "Still no cookies for me"); + }, + + // Non blocking callback + async _ => { + is(document.cookie, "", "No cookies for me"); + + // Note: The ?redirect test is _not_ using checkonly, so it will actually + // set our foopy=1 cookie. + for (let arg of ["?checkonly", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + } + + ok(document.cookie.length, "Some Cookies for me"); + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "Cookies and Storage Access API", + // Blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + is(document.cookie, "", "Still no cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + is(document.cookie, "", "No cookies for me"); + } else { + is(document.cookie, "name=value", "I have the cookies!"); + } + }, + + // Non blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + + // Note: The ?redirect test is _not_ using checkonly, so it will actually + // set our foopy=1 cookie. + for (let arg of ["?checkonly", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + } + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + for (let arg of ["", "?redirect"]) { + info(`checking with arg=${arg}`); + await fetch("server.sjs" + arg) + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + } + + ok(document.cookie.length, "Some Cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + ok(document.cookie.length, "Still some Cookies for me"); + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js new file mode 100644 index 0000000000..a8353cc8f1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCache.js @@ -0,0 +1,39 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache", + async _ => { + await caches.open("wow").then( + _ => { + ok(false, "DOM Cache cannot be used!"); + }, + _ => { + ok(true, "DOM Cache cannot be used!"); + } + ); + }, + async _ => { + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js new file mode 100644 index 0000000000..4cb90af8ad --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartition.js @@ -0,0 +1,54 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache Always Partition Storage", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + // caches.open still fails for cookieBehavior 2 (reject) and + // 1 (reject for third parties), so we need to account for that. + let shouldFail = + effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT || + (effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN && + !document.location.href.includes("3rdPartyWO") && + !document.location.href.includes("3rdPartyUI")); + + await caches.open("wow").then( + _ => { + ok(!shouldFail, "DOM Cache can be used!"); + }, + _ => { + ok(shouldFail, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + ["privacy.partition.always_partition_third_party_non_cookie_storage", true], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js new file mode 100644 index 0000000000..bef53f2936 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheAlwaysPartitionSAA.js @@ -0,0 +1,80 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache Always Partition Storage and Storage Access API", + async _ => { + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + + await callRequestStorageAccess(); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + ["privacy.partition.always_partition_third_party_non_cookie_storage", true], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js new file mode 100644 index 0000000000..06526972dd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingDOMCacheSAA.js @@ -0,0 +1,85 @@ +requestLongerTimeout(2); + +AntiTracking.runTest( + "DOM Cache and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(false, "DOM Cache cannot be used!"); + }, + _ => { + ok(true, "DOM Cache cannot be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + await caches.open("wow").then( + _ => { + ok(!shouldThrow, "DOM Cache can be used!"); + }, + _ => { + ok(shouldThrow, "DOM Cache can be used!"); + } + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.caches.testing.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js new file mode 100644 index 0000000000..7fde2365bb --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDb.js @@ -0,0 +1,102 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB", + // blocking callback + async _ => { + try { + indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + // non-blocking callback + async _ => { + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]] +); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB and Storage Access API", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + indexedDB.open("test", "1"); + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "IDB should be allowed if not in cookieBehavior pref value BEHAVIOR_REJECT/BEHAVIOR_REJECT_FOREIGN" + ); + }, + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js new file mode 100644 index 0000000000..55db3cc05b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers.js @@ -0,0 +1,73 @@ +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB in workers", + async _ => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js new file mode 100644 index 0000000000..02ce8dc588 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingIndexedDbInWorkers2.js @@ -0,0 +1,152 @@ +requestLongerTimeout(6); + +AntiTracking.runTestInNormalAndPrivateMode( + "IndexedDB in workers and Storage Access API", + async _ => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let blob = new Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + blob = new Blob([blockCode.toString() + "; blockCode();"]); + } else { + blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + } + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + let blob = new Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["dom.indexedDB.hide_in_pbmode.enabled", false]], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js new file mode 100644 index 0000000000..1362fc7519 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingLocalStorage.js @@ -0,0 +1,94 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js new file mode 100644 index 0000000000..34af2b76ce --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingMessaging.js @@ -0,0 +1,322 @@ +if (AppConstants.MOZ_CODE_COVERAGE) { + requestLongerTimeout(12); +} else { + requestLongerTimeout(12); +} + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel", + async _ => { + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel be used"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel in workers", + async _ => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + new BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + new BroadcastChannel("hello"); + ok(true, "BroadcastChanneli can be used"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + new BroadcastChannel("hello"); + ok(true, "BroadcastChannel can be used"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); + +AntiTracking.runTestInNormalAndPrivateMode( + "BroadcastChannel in workers and Storage Access API", + async _ => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + blob = new Blob([blockingCode.toString() + "; blockingCode();"]); + } else { + blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + } + + ok(blob, "Blob has been created"); + + blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + let blob = new Blob([nonBlockingCode.toString() + "; nonBlockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + + worker = new Worker(blobURL); + ok(worker, "Worker has been created"); + + await new Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js new file mode 100644 index 0000000000..ba75454e18 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingNoOpener.js @@ -0,0 +1,41 @@ +gFeatures = "noopener"; + +AntiTracking.runTestInNormalAndPrivateMode( + "Blocking in the case of noopener windows", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async phase => { + switch (phase) { + case 1: + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + break; + case 2: + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + break; + } + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + true, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js new file mode 100644 index 0000000000..c61f85d69f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkers.js @@ -0,0 +1,30 @@ +AntiTracking.runTest( + "ServiceWorkers", + async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + null, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js new file mode 100644 index 0000000000..a7676bf939 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingServiceWorkersStorageAccessAPI.js @@ -0,0 +1,133 @@ +/* import-globals-from antitracking_head.js */ + +requestLongerTimeout(2); + +AntiTracking.runTest( + "ServiceWorkers and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + } else { + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used! " + _); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used!"); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + await navigator.serviceWorker + .register("empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used!"); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js new file mode 100644 index 0000000000..25a926ed3f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingSessionStorage.js @@ -0,0 +1,126 @@ +requestLongerTimeout(6); + +AntiTracking.runTestInNormalAndPrivateMode( + "sessionStorage", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + }, + async _ => { + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [], + true, + true +); + +AntiTracking.runTestInNormalAndPrivateMode( + "sessionStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + try { + sessionStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + is(e.name, "SecurityError", "We want a security error message."); + } + + is( + hasThrown, + shouldThrow, + "SessionStorage show thrown only if cookieBehavior is REJECT" + ); + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + sessionStorage.foo = 42; + ok(true, "SessionStorage is always allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + sessionStorage.foo = 42; + ok( + true, + "SessionStorage is allowed after calling the storage access API too" + ); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js new file mode 100644 index 0000000000..0daffc4565 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_blockingSharedWorkers.js @@ -0,0 +1,94 @@ +requestLongerTimeout(4); + +AntiTracking.runTestInNormalAndPrivateMode( + "SharedWorkers", + async _ => { + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +AntiTracking.runTestInNormalAndPrivateMode( + "SharedWorkers and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + if ( + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior) + ) { + try { + new SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + } else { + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + new SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + new SharedWorker("a.js", "bar"); + ok(true, "SharedWorker is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, + false, + false +); diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js new file mode 100644 index 0000000000..a9e2ac6fce --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingAllowListPrincipal.js @@ -0,0 +1,237 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_SANDBOX_URL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/sandboxed.html"; + +/** + * Tests the contentBlockingAllowListPrincipal. + * @param {Browser} browser - Browser to test. + * @param {("content"|"system")} type - Expected principal type. + * @param {String} [origin] - Expected origin of principal. Defaults to the + * origin of the browsers content principal. + */ +function checkAllowListPrincipal( + browser, + type, + origin = browser.contentPrincipal.origin +) { + let principal = + browser.browsingContext.currentWindowGlobal + .contentBlockingAllowListPrincipal; + ok(principal, "Principal is set"); + + if (type == "content") { + ok(principal.isContentPrincipal, "Is content principal"); + + ok( + principal.schemeIs("https"), + "allowlist content principal must have https scheme" + ); + } else if (type == "system") { + ok(principal.isSystemPrincipal, "Is system principal"); + } else { + throw new Error("Unexpected principal type"); + } + + is(principal.origin, origin, "Correct origin"); +} + +/** + * Runs a given test in a normal window and in a private browsing window. + * @param {String} initialUrl - URL to load in the initial test tab. + * @param {Function} testCallback - Test function to run in both windows. + */ +async function runTestInNormalAndPrivateMode(initialUrl, testCallback) { + for (let i = 0; i < 2; i++) { + let isPrivateBrowsing = !!i; + info("Running test. Private browsing: " + !!i); + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: isPrivateBrowsing, + }); + let tab = BrowserTestUtils.addTab(win.gBrowser, initialUrl); + let browser = tab.linkedBrowser; + + await BrowserTestUtils.browserLoaded(browser); + + await testCallback(browser, isPrivateBrowsing); + + await BrowserTestUtils.closeWindow(win); + } +} + +/** + * 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 {String} [sandboxAttr] - Optional list of sandbox attributes to set + * for the iframe. Defaults to no sandbox. + * @returns {Promise} - Resolves once the frame has loaded. + */ +function createFrame(browser, src, id, sandboxAttr) { + return SpecialPowers.spawn( + browser, + [{ page: src, frameId: id, sandboxAttr }], + async function (obj) { + await new content.Promise(resolve => { + let frame = content.document.createElement("iframe"); + frame.src = obj.page; + frame.id = obj.frameId; + if (obj.sandboxAttr) { + frame.setAttribute("sandbox", obj.sandboxAttr); + } + frame.addEventListener("load", resolve, { once: true }); + content.document.body.appendChild(frame); + }); + } + ); +} + +add_task(async setup => { + // Disable heuristics. We don't need them and if enabled the resulting + // telemetry can race with the telemetry in the next test. + // See Bug 1686836, Bug 1686894. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.redirect", false], + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ["privacy.restrict3rdpartystorage.heuristic.window_open", false], + ["dom.security.https_first_pbm", false], + ], + }); +}); + +/** + * Test that we get the correct allow list principal which matches the content + * principal for an https site. + */ +add_task(async test_contentPrincipalHTTPS => { + await runTestInNormalAndPrivateMode("https://example.com", browser => { + checkAllowListPrincipal(browser, "content"); + }); +}); + +/** + * Tests that the scheme of the allowlist principal is HTTPS, even though the + * site is loaded via HTTP. + */ +add_task(async test_contentPrincipalHTTP => { + await runTestInNormalAndPrivateMode( + "http://example.net", + (browser, isPrivateBrowsing) => { + checkAllowListPrincipal( + browser, + "content", + "https://example.net" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + } + ); +}); + +/** + * Tests that the allow list principal is a system principal for the preferences + * about site. + */ +add_task(async test_systemPrincipal => { + await runTestInNormalAndPrivateMode("about:preferences", browser => { + checkAllowListPrincipal(browser, "system"); + }); +}); + +/** + * Tests that we get a valid content principal for top level sandboxed pages, + * and not the document principal which is a null principal. + */ +add_task(async test_TopLevelSandbox => { + await runTestInNormalAndPrivateMode( + TEST_SANDBOX_URL, + (browser, isPrivateBrowsing) => { + ok( + browser.contentPrincipal.isNullPrincipal, + "Top level sandboxed page should have null principal" + ); + checkAllowListPrincipal( + browser, + "content", + "https://example.com" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + } + ); +}); + +/** + * Tests that we get a valid content principal for a new tab opened via + * window.open. + */ +add_task(async test_windowOpen => { + await runTestInNormalAndPrivateMode("https://example.com", async browser => { + checkAllowListPrincipal(browser, "content"); + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + browser.ownerGlobal.gBrowser, + "https://example.org/", + true + ); + + // Call window.open from iframe. + await SpecialPowers.spawn(browser, [], async function () { + content.open("https://example.org/"); + }); + + let tab = await promiseTabOpened; + + checkAllowListPrincipal(tab.linkedBrowser, "content"); + + BrowserTestUtils.removeTab(tab); + }); +}); + +/** + * Tests that we get a valid content principal for a new tab opened via + * window.open from a sandboxed iframe. + */ +add_task(async test_windowOpenFromSandboxedFrame => { + await runTestInNormalAndPrivateMode( + "https://example.com", + async (browser, isPrivateBrowsing) => { + checkAllowListPrincipal(browser, "content"); + + // Create sandboxed iframe, allow popups. + await createFrame( + browser, + "https://example.com", + "sandboxedIframe", + "allow-popups" + ); + // Iframe BC is the only child of the test browser. + let [frameBrowsingContext] = browser.browsingContext.children; + + let promiseTabOpened = BrowserTestUtils.waitForNewTab( + browser.ownerGlobal.gBrowser, + "https://example.org/", + true + ); + + // Call window.open from iframe. + await SpecialPowers.spawn(frameBrowsingContext, [], async function () { + content.open("https://example.org/"); + }); + + let tab = await promiseTabOpened; + + checkAllowListPrincipal( + tab.linkedBrowser, + "content", + "https://example.org" + + (isPrivateBrowsing ? "^privateBrowsingId=1" : "") + ); + + BrowserTestUtils.removeTab(tab); + } + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js new file mode 100644 index 0000000000..dc7bf0c80e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_contentBlockingTelemetry.js @@ -0,0 +1,404 @@ +/** + * Bug 1668199 - Testing the content blocking telemetry. + */ + +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const LABEL_STORAGE_GRANTED = 0; +const LABEL_STORAGE_ACCESS_API = 1; +const LABEL_OPENER_AFTER_UI = 2; +const LABEL_OPENER = 3; +const LABEL_REDIRECT = 4; + +function clearTelemetry() { + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById("STORAGE_ACCESS_REMAINING_DAYS").clear(); +} + +const expectedExpiredDays = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration" +); +const expectedExpiredDaysRedirect = getExpectedExpiredDaysFromPref( + "privacy.restrict3rdpartystorage.expiration_redirect" +); + +function getExpectedExpiredDaysFromPref(pref) { + let expiredSecond = Services.prefs.getIntPref(pref); + + // This is unlikely to happen, but just in case. + if (expiredSecond <= 0) { + return 0; + } + + // We need to subtract one second from the expect expired second because there + // will be a short delay between the time we add the permission and the time + // we record the telemetry. Subtracting one can help us to get the correct + // expected expired days. + // + // Note that 86400 is seconds in one day. + return Math.trunc((expiredSecond - 1) / 86400); +} + +async function testTelemetry( + aProbeInParent, + aExpectedCnt, + aLabel, + aExpectedIdx +) { + info("Trigger the 'idle-daily' to trigger the telemetry probe."); + // Synthesis a fake 'idle-daily' notification to the content blocking + // telemetry service. + let cbts = Cc["@mozilla.org/content-blocking-telemetry-service;1"].getService( + Ci.nsIObserver + ); + cbts.observe(null, "idle-daily", null); + + let storageAccessGrantedHistogram; + + // Wait until the telemetry probe appears. + await BrowserTestUtils.waitForCondition(() => { + let histograms; + if (aProbeInParent) { + histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + } else { + histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + } + storageAccessGrantedHistogram = histograms.STORAGE_ACCESS_GRANTED_COUNT; + + return ( + !!storageAccessGrantedHistogram && + storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED] == + aExpectedCnt + ); + }); + + is( + storageAccessGrantedHistogram.values[LABEL_STORAGE_GRANTED], + aExpectedCnt, + "There should be expected storage access granted count in telemetry." + ); + is( + storageAccessGrantedHistogram.values[aLabel], + 1, + "There should be one reason count in telemetry." + ); + + let storageAccessRemainingDaysHistogram = Services.telemetry.getHistogramById( + "STORAGE_ACCESS_REMAINING_DAYS" + ); + + TelemetryTestUtils.assertHistogram( + storageAccessRemainingDaysHistogram, + aExpectedIdx, + 1 + ); + + // Clear telemetry probes + clearTelemetry(); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT_TRACKER], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", true], + ["toolkit.telemetry.ipcBatchTimeout", 0], + ], + }); + + // Clear Telemetry probes before testing. + // There can be telemetry race conditions if the previous test generates + // content blocking telemetry. + // See Bug 1686836, Bug 1686894. + clearTelemetry(); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +add_task(async function testTelemetryForStorageAccessAPI() { + info("Starting testing if storage access API send telemetry probe ..."); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking iframe and call the RequestStorageAccess."); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + }, + ], + async obj => { + let msg = {}; + msg.callback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + }).toString(); + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + await testTelemetry(false, 1, LABEL_STORAGE_ACCESS_API, expectedExpiredDays); +}); + +add_task(async function testTelemetryForWindowOpenHeuristic() { + info("Starting testing if window open heuristic send telemetry probe ..."); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking iframe and trigger the heuristic"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + await testTelemetry(false, 1, LABEL_OPENER, expectedExpiredDays); +}); + +add_task(async function testTelemetryForUserInteractionHeuristic() { + info( + "Starting testing if UserInteraction heuristic send telemetry probe ..." + ); + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Interact with the tracker in top-level."); + await AntiTracking.interactWithTracker(); + + info("Loading the tracking iframe and trigger the heuristic"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI here as well for the same + // reason above. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + // The storage access permission will be expired in 29 days, so the expected + // index in the telemetry probe would be 29. + // + // Note that the expected count here is 2. It's because the opener heuristic + // will also be triggered when triggered UserInteraction Heuristic. + await testTelemetry(false, 2, LABEL_OPENER_AFTER_UI, expectedExpiredDays); +}); + +add_task(async function testTelemetryForRedirectHeuristic() { + info("Starting testing if redirect heuristic send telemetry probe ..."); + + const TEST_TRACKING_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "page.html"; + const TEST_REDIRECT_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE; + + // First, clear all permissions. + Services.perms.removeAll(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TRACKING_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading the tracking page and trigger the redirect."); + SpecialPowers.spawn(browser, [TEST_REDIRECT_PAGE], async url => { + content.document.userInteractionForTesting(); + + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = url; + content.document.body.appendChild(link); + link.click(); + }); + + await BrowserTestUtils.browserLoaded(browser, false, TEST_TOP_PAGE); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + // We would only grant the storage permission for 29 days for the redirect + // heuristic, so the expected index in the telemetry probe would be 29. + await testTelemetry(true, 1, LABEL_REDIRECT, expectedExpiredDaysRedirect); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js new file mode 100644 index 0000000000..635f145d46 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_cookieBetweenTabs.js @@ -0,0 +1,59 @@ +add_task(async function () { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["network.cookie.cookieBehavior.pbmode", BEHAVIOR_REJECT], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["dom.ipc.processCount", 4], + ], + }); + + info("First tab opened"); + let tab = BrowserTestUtils.addTab( + gBrowser, + TEST_DOMAIN + TEST_PATH + "empty.html" + ); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Disabling content blocking for this page"); + gProtectionsHandler.disableForCurrentPage(); + + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async _ => { + is(content.document.cookie, "", "No cookie set"); + content.document.cookie = "a=b"; + is(content.document.cookie, "a=b", "Cookie set"); + }); + + info("Second tab opened"); + let tab2 = BrowserTestUtils.addTab( + gBrowser, + TEST_DOMAIN + TEST_PATH + "empty.html" + ); + gBrowser.selectedTab = tab2; + + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + await SpecialPowers.spawn(browser2, [], async _ => { + is(content.document.cookie, "a=b", "Cookie set"); + }); + + info("Removing tabs"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js new file mode 100644 index 0000000000..cd19fa4466 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_denyPermissionForTracker.js @@ -0,0 +1,64 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "https://tracking.example.org", + "cookie", + Services.perms.DENY_ACTION + ); + PermissionTestUtils.add( + "https://tracking.example.com", + "cookie", + Services.perms.DENY_ACTION + ); + // Grant interaction permission so we can directly call + // requestStorageAccess from the tracker. + PermissionTestUtils.add( + "https://tracking.example.org", + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do honour a cookie permission for nested windows", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + document.cookie = "name=value"; + ok(document.cookie == "", "All is blocked"); + + // requestStorageAccess should reject + SpecialPowers.wrap(document).notifyUserGestureActivation(); + await document + .requestStorageAccess() + .then(() => { + ok(false, "Should not grant storage access"); + }) + .catch(() => { + ok(true, "Should not grant storage access"); + }); + SpecialPowers.wrap(document).clearUserGestureActivation(); + }, + extraPrefs: null, + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js new file mode 100644 index 0000000000..c15e3abd5f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_doublyNestedTracker.js @@ -0,0 +1,130 @@ +add_task(async function () { + info("Starting doubly nested tracker test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + async function loadSubpage() { + async function runChecks() { + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "I have the cookies!"); + } + + await new Promise(resolve => { + let ifr = document.createElement("iframe"); + ifr.onload = _ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(runChecks.toString(), "*"); + }; + + addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + document.body.appendChild(ifr); + ifr.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdParty.html"; + }); + } + + // We need to use the same scheme in Fission test. + let testAnotherThirdPartyPage = SpecialPowers.useRemoteSubframes + ? TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS + : TEST_ANOTHER_3RD_PARTY_PAGE; + + await SpecialPowers.spawn( + browser, + [{ page: testAnotherThirdPartyPage, callback: loadSubpage.toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = _ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_emailtracking.js b/toolkit/components/antitracking/test/browser/browser_emailtracking.js new file mode 100644 index 0000000000..90616aba6d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_emailtracking.js @@ -0,0 +1,182 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +add_setup(async function () { + // Disable other tracking protection feature to avoid interfering with the + // current test. This also setup prefs for testing email tracking. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", false], + ["privacy.trackingprotection.cryptomining.enabled", false], + ["privacy.trackingprotection.emailtracking.enabled", true], + ["privacy.trackingprotection.fingerprinting.enabled", false], + ["privacy.trackingprotection.socialtracking.enabled", false], + [ + "urlclassifier.features.emailtracking.blocklistTables", + "mochitest5-track-simple", + ], + ["urlclassifier.features.emailtracking.allowlistTables", ""], + [ + "urlclassifier.features.emailtracking.datacollection.blocklistTables", + "mochitest5-track-simple", + ], + [ + "urlclassifier.features.emailtracking.datacollection.allowlistTables", + "", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(_ => { + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +}); + +function runTest(obj) { + add_task(async _ => { + info("Test: " + obj.testName); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.trackingprotection.emailtracking.enabled", + obj.protectionEnabled, + ], + [ + "privacy.trackingprotection.emailtracking.pbmode.enabled", + obj.protectionPrivateEnabled, + ], + ], + }); + + let win; + + if (obj.testPrivate) { + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + } else { + win = window; + } + + info("Creating a non-tracker top-level context"); + let tab = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + info("The non-tracker page opens an email tracker iframe"); + await SpecialPowers.spawn( + browser, + [ + { + image: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "raptor.jpg", + script: TEST_EMAIL_TRACKER_DOMAIN + TEST_PATH + "empty.js", + loading: obj.loading, + }, + ], + async obj => { + info("Image loading ..."); + let loading = await new content.Promise(resolve => { + let image = new content.Image(); + image.src = obj.image + "?" + Math.random(); + image.onload = _ => resolve(true); + image.onerror = _ => resolve(false); + }); + + is(loading, obj.loading, "Image loading expected"); + + let script = content.document.createElement("script"); + script.setAttribute("src", obj.script); + + info("Script loading ..."); + loading = await new content.Promise(resolve => { + script.onload = _ => resolve(true); + script.onerror = _ => resolve(false); + content.document.body.appendChild(script); + }); + + is(loading, obj.loading, "Script loading expected"); + } + ); + + info("Checking content blocking log."); + let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog()); + let origins = Object.keys(contentBlockingLog); + is(origins.length, 1, "There should be one origin entry in the log."); + for (let origin of origins) { + is( + origin + "/", + TEST_EMAIL_TRACKER_DOMAIN, + "Correct tracker origin must be reported" + ); + Assert.deepEqual( + contentBlockingLog[origin], + obj.expectedLogItems, + "Content blocking log should be as expected" + ); + } + + BrowserTestUtils.removeTab(tab); + if (obj.testPrivate) { + await BrowserTestUtils.closeWindow(win); + } + await SpecialPowers.popPrefEnv(); + }); +} + +runTest({ + testName: + "EmailTracking-dataCollection feature enabled but not considered for tracking detection.", + protectionEnabled: false, + protectionPrivateEnabled: false, + loading: true, + expectedLogItems: [ + [ + Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT, + true, + 2, + ], + ], +}); + +runTest({ + testName: "Emailtracking-protection feature enabled.", + protectionEnabled: true, + protectionPrivateEnabled: true, + loading: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 2], + ], +}); + +runTest({ + testName: + "Emailtracking-protection feature enabled for private windows and doesn't block in normal windows", + protectionEnabled: false, + protectionPrivateEnabled: true, + loading: true, + expectedLogItems: [ + [ + Ci.nsIWebProgressListener.STATE_LOADED_EMAILTRACKING_LEVEL_1_CONTENT, + true, + 2, + ], + ], +}); + +runTest({ + testName: + "Emailtracking-protection feature enabled for private windows and block in private windows", + testPrivate: true, + protectionEnabled: true, + protectionPrivateEnabled: true, + loading: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_EMAILTRACKING_CONTENT, true, 1], + ], +}); diff --git a/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js new file mode 100644 index 0000000000..35e9dfb169 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_existingCookiesForSubresources.js @@ -0,0 +1,235 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_3RD_PARTY_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info( + "Loading tracking scripts and tracking images before restricting 3rd party cookies" + ); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "Cookies received for images"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "Cookies received for scripts"); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + Services.perms.removeAll(); + + // Now set up our prefs + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + info("Creating a new tab"); + tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [{ page: TEST_3RD_PARTY_PAGE, callback: (async _ => {}).toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading tracking scripts and tracking images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "No cookie received for images."); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "No cookie received received for scripts."); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_fileUrl.js b/toolkit/components/antitracking/test/browser/browser_fileUrl.js new file mode 100644 index 0000000000..509c143a9b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_fileUrl.js @@ -0,0 +1,41 @@ +/** + * Bug 1663192 - Testing for ensuring the top-level window in a fire url is + * treated as first-party. + */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", 1], + ["network.cookie.cookieBehavior.pbmode", 1], + ], + }); +}); + +add_task(async function () { + let dir = getChromeDir(getResolvedURI(gTestPath)); + dir.append("file_localStorage.html"); + const uriString = Services.io.newFileURI(dir).spec; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, uriString); + + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + let result = content.document.getElementById("result"); + + is( + result.textContent, + "PASS", + "The localStorage is accessible in top-level window" + ); + + let loadInfo = content.docShell.currentDocumentChannel.loadInfo; + + ok( + !loadInfo.isThirdPartyContextToTopWindow, + "The top-level window shouldn't be third-party" + ); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js new file mode 100644 index 0000000000..d3d06d2950 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_firstPartyCookieRejectionHonoursAllowList.js @@ -0,0 +1,77 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Disabling content blocking for this page"); + gProtectionsHandler.disableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [], async function (obj) { + await new content.Promise(async resolve => { + let document = content.document; + let window = document.defaultView; + + is(document.cookie, "", "No cookies for me"); + + await window + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + document.cookie = "name=value"; + ok(document.cookie.includes("name=value"), "Some cookies for me"); + ok(document.cookie.includes("foopy=1"), "Some cookies for me"); + + await window + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + + ok(document.cookie.length, "Some Cookies for me"); + + resolve(); + }); + }); + + info("Enabling content blocking for this page"); + gProtectionsHandler.enableForCurrentPage(); + + // The previous function reloads the browser, so wait for it to load again! + await BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js new file mode 100644 index 0000000000..fcdc83cde6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_fpiServiceWorkers_fingerprinting.js @@ -0,0 +1,90 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +// This test ensures that a service worker for an exempted domain is exempted when it is +// in the first party context, and not exempted when it is in a third party context. + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Check that RFP correctly is exempted and not exempted when FPI is enabled", + async (win3rdParty, win1stParty, allowed) => { + // Quickly unset and reset these prefs so we can get the real navigator.hardwareConcurrency + // It can't be set enternally and then captured because this function gets stringified and then evaled in a new scope. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.firstParty.isolate", false], + ["privacy.resistFingerprinting", false], + ], + }); + + var SPOOFED_HW_CONCURRENCY = 2; + var DEFAULT_HARDWARE_CONCURRENCY = navigator.hardwareConcurrency; + + await SpecialPowers.popPrefEnv(); + + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Check DOM cache from the first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "First Party, got: " + + res.value + + " Expected: " + + DEFAULT_HARDWARE_CONCURRENCY + ); + is( + res.value, + DEFAULT_HARDWARE_CONCURRENCY, + "As a first party, HW Concurrency should not be spoofed" + ); + + // Check DOM cache from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetHWConcurrency" } + ); + console.info( + "Third Party, got: " + res.value + " Expected: " + SPOOFED_HW_CONCURRENCY + ); + is( + res.value, + SPOOFED_HW_CONCURRENCY, + "As a third party, HW Concurrency should be spoofed" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.firstParty.isolate", true], + ["privacy.resistFingerprinting", true], + ["privacy.resistFingerprinting.exemptedDomains", "*.example.com"], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js new file mode 100644 index 0000000000..c51959fe78 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess.js @@ -0,0 +1,196 @@ +// This test ensures HasStorageAccess API returns the right value under different +// scenarios. + +var settings = [ + // same-origin no-tracker + { + name: "Test whether same-origin non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + }, + // 3rd-party no-tracker with permission + { + name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // 3rd-party tracker + { + name: "Test whether 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + }, + // 3rd-party tracker with permission + { + name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^https://tracking.example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // same-site 3rd-party tracker + { + name: "Test whether same-site 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, +]; + +var testCases = [ + { + behavior: BEHAVIOR_ACCEPT, // 0 + hasStorageAccess: [ + true /* same-origin non-tracker */, + true /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + true /* 3rd-party tracker */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ) /* 3rd-party tracker with permission */, + false /* 3rd-party tracker */, + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ) /* 3rd-party non-tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT, // 2 + hasStorageAccess: [ + false /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + false /* same-site tracker */, + false /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_LIMIT_FOREIGN, // 3 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + false /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + false /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER, // 4 + hasStorageAccess: [ + true /* same-origin non-tracker */, + true /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + hasStorageAccess: [ + true /* same-origin non-tracker */, + false /* 3rd-party non-tracker */, + true /* 3rd-party non-tracker with permission */, + false /* 3rd-party tracker */, + true /* 3rd-party tracker with permission */, + true /* same-site tracker */, + true /* same-origin tracker */, + ], + }, +]; + +(function () { + settings.forEach(setting => { + if (setting.setup) { + add_task(async _ => { + setting.setup(); + }); + } + + testCases.forEach(test => { + let callback = test.hasStorageAccess[settings.indexOf(setting)] + ? async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + } + : async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }; + + AntiTracking._createTask({ + name: setting.name, + cookieBehavior: test.behavior, + allowList: false, + callback, + extraPrefs: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: setting.topPage, + thirdPartyPage: setting.thirdPartyPage, + }); + }); + + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); + }); +})(); diff --git a/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js new file mode 100644 index 0000000000..d89b0b1103 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_hasStorageAccess_alwaysPartition.js @@ -0,0 +1,209 @@ +// This test ensures HasStorageAccess API returns the right value under different +// scenarios. + +var settings = [ + // same-origin no-tracker + { + name: "Test whether same-origin non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + }, + // 3rd-party no-tracker + { + name: "Test whether 3rd-party non-tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + }, + // 3rd-party no-tracker with permission + { + name: "Test whether 3rd-party non-tracker frame has storage access when storage permission is granted before", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // 3rd-party tracker + { + name: "Test whether 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + }, + // 3rd-party tracker with permission + { + name: "Test whether 3rd-party tracker frame has storage access when storage access permission is granted before", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_3RD_PARTY_PAGE, + setup: () => { + let type = "3rdPartyStorage^https://tracking.example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + }, + }, + // same-site 3rd-party tracker + { + name: "Test whether same-site 3rd-party tracker frame has storage access", + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, + // same-origin 3rd-party tracker + { + name: "Test whether same-origin 3rd-party tracker frame has storage access", + topPage: TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "page.html", + thirdPartyPage: TEST_ANOTHER_3RD_PARTY_PAGE, + }, +]; + +const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL; +const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN; +const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +var testCases = [ + { + behavior: BEHAVIOR_ACCEPT, // 0 + cases: [ + [true] /* same-origin non-tracker */, + [true] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [true] /* 3rd-party tracker */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_FOREIGN, // 1 + cases: [ + [true] /* same-origin non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker */, + [ + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ), + foreignBlocked, + ] /* 3rd-party tracker with permission */, + [false, foreignBlocked] /* 3rd-party tracker */, + [ + SpecialPowers.Services.prefs.getBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled" + ), + foreignBlocked, + ] /* 3rd-party non-tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT, // 2 + cases: [ + [false, allBlocked] /* same-origin non-tracker */, + [false, allBlocked] /* 3rd-party non-tracker */, + [false, allBlocked] /* 3rd-party non-tracker with permission */, + [false, allBlocked] /* 3rd-party tracker */, + [false, allBlocked] /* 3rd-party tracker with permission */, + [false, allBlocked] /* same-site tracker */, + [false, allBlocked] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_LIMIT_FOREIGN, // 3 + cases: [ + [true] /* same-origin non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker */, + [false, foreignBlocked] /* 3rd-party non-tracker with permission */, + [false, foreignBlocked] /* 3rd-party tracker */, + [false, foreignBlocked] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER, // 4 + cases: [ + [true] /* same-origin non-tracker */, + [true] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [false, trackerBlocked] /* 3rd-party tracker */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, + { + behavior: BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, // 5 + cases: [ + [true] /* same-origin non-tracker */, + [false] /* 3rd-party non-tracker */, + [true] /* 3rd-party non-tracker with permission */, + [false, trackerBlocked] /* 3rd-party tracker */, + [true] /* 3rd-party tracker with permission */, + [true] /* same-site tracker */, + [true] /* same-origin tracker */, + ], + }, +]; + +(function () { + settings.forEach(setting => { + ok(true, JSON.stringify(setting)); + if (setting.setup) { + add_task(async _ => { + setting.setup(); + }); + } + + testCases.forEach(test => { + let [hasStorageAccess, expectedBlockingNotifications] = + test.cases[settings.indexOf(setting)]; + let callback = hasStorageAccess + ? async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + } + : async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }; + + AntiTracking._createTask({ + name: setting.name, + cookieBehavior: test.behavior, + allowList: false, + callback, + extraPrefs: [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ], + expectedBlockingNotifications, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: setting.topPage, + thirdPartyPage: setting.thirdPartyPage, + }); + }); + + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); + }); +})(); diff --git a/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js new file mode 100644 index 0000000000..73876ee1a5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_iframe_document_open.js @@ -0,0 +1,86 @@ +// +// Bug 1725996 - Test if the cookie set in a document which is created by +// document.open() in an about:blank iframe has a correct +// partitionKey +// + +const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "file_iframe_document_open.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.dynamic_firstparty.use_site", true], + ], + }); +}); + +add_task(async function test_firstParty_iframe() { + // Clear all cookies before test. + Services.cookies.removeAll(); + + // Open the test page which creates an iframe and then calls document.write() + // to write a cookie in the iframe. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE); + + // Wait until the cookie appears. + await TestUtils.waitForCondition(_ => Services.cookies.cookies.length); + + // Check the partitionKey in the cookie. + let cookie = Services.cookies.cookies[0]; + is( + cookie.originAttributes.partitionKey, + "", + "The partitionKey should remain empty for first-party iframe." + ); + + // Clean up. + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_thirdParty_iframe() { + // Clear all cookies before test. + Services.cookies.removeAll(); + + // Open a tab with a different domain with the test page. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE_2 + ); + + // Open the test page within a third-party context. + await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_PAGE], + async function (page) { + let ifr = content.document.createElement("iframe"); + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + ifr.src = page; + content.document.body.appendChild(ifr); + await loading; + } + ); + + // Wait until the cookie appears. + await TestUtils.waitForCondition(_ => Services.cookies.cookies.length); + + // Check the partitionKey in the cookie. + let cookie = Services.cookies.cookies[0]; + is( + cookie.originAttributes.partitionKey, + "(http,xn--exmple-cua.test)", + "The partitionKey should exist for third-party iframe." + ); + + // Clean up. + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache4.js b/toolkit/components/antitracking/test/browser/browser_imageCache4.js new file mode 100644 index 0000000000..8fcc298cf0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_imageCache4.js @@ -0,0 +1,13 @@ +let cookieBehavior = BEHAVIOR_REJECT_TRACKER; +let blockingByAllowList = false; +let expectedBlockingNotifications = + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from imageCacheWorker.js */ +Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this); diff --git a/toolkit/components/antitracking/test/browser/browser_imageCache8.js b/toolkit/components/antitracking/test/browser/browser_imageCache8.js new file mode 100644 index 0000000000..b57bf1dca5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_imageCache8.js @@ -0,0 +1,13 @@ +let cookieBehavior = BEHAVIOR_REJECT_TRACKER; +let blockingByAllowList = true; +let expectedBlockingNotifications = + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +let rootDir = getRootDirectory(gTestPath); +let jar = getJar(rootDir); +if (jar) { + let tmpdir = extractJarToTmp(jar); + rootDir = "file://" + tmpdir.path + "/"; +} +/* import-globals-from imageCacheWorker.js */ +Services.scriptloader.loadSubScript(rootDir + "imageCacheWorker.js", this); diff --git a/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js new file mode 100644 index 0000000000..b46cc60b91 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_localStorageEvents.js @@ -0,0 +1,186 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); +}); + +add_task(async function testLocalStorageEventPropagation() { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html", + }, + ], + async obj => { + info("Creating tracker iframe"); + + let ifr = content.document.createElement("iframe"); + ifr.src = obj.page; + + await new content.Promise(resolve => { + ifr.onload = function () { + resolve(); + }; + content.document.body.appendChild(ifr); + }); + + info("LocalStorage should be blocked."); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("test", "*"); + }); + + info("Let's open the popup"); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, true, "LocalStorage unblocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("open", "*"); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function testBlockedLocalStorageEventPropagation() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "localStorage.html", + }, + ], + async obj => { + info("Creating tracker iframe"); + + let ifr = content.document.createElement("iframe"); + ifr.src = obj.page; + + await new content.Promise(resolve => { + ifr.onload = function () { + resolve(); + }; + content.document.body.appendChild(ifr); + }); + + info("LocalStorage should be blocked."); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("test", "*"); + }); + + info("Let's open the popup"); + await new content.Promise(resolve => { + content.addEventListener( + "message", + e => { + if (e.data.type == "test") { + is(e.data.status, false, "LocalStorage still blocked"); + } else { + ok(false, "Unknown message"); + } + resolve(); + }, + { once: true } + ); + ifr.contentWindow.postMessage("open and test", "*"); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js new file mode 100644 index 0000000000..847bfd7dc9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_onBeforeRequestNotificationForTrackingResources.js @@ -0,0 +1,96 @@ +/** + * This test ensures that onBeforeRequest is dispatched for webRequest loads that + * are blocked by tracking protection. It sets up a page with a third-party script + * resource on it that is blocked by TP, and sets up an onBeforeRequest listener + * which waits to be notified about that resource. The test would time out if the + * onBeforeRequest listener isn't called dispatched before the load is canceled. + */ + +let extension; +add_task(async function () { + extension = ExtensionTestUtils.loadExtension({ + manifest: { permissions: ["webRequest", "webRequestBlocking", "*://*/*"] }, + async background() { + let gExpectedResourcesSeen = 0; + function onBeforeRequest(details) { + let spec = details.url; + browser.test.log("Observed channel for " + spec); + // We would use TEST_3RD_PARTY_DOMAIN_TP here, but the variable is inaccessible + // since it is defined in head.js! + if (!spec.startsWith("https://tracking.example.com/")) { + return undefined; + } + if (spec.endsWith("empty.js")) { + browser.test.succeed("Correct resource observed"); + ++gExpectedResourcesSeen; + } else if (spec.endsWith("empty.js?redirect")) { + return { redirectUrl: spec.replace("empty.js?redirect", "head.js") }; + } else if (spec.endsWith("head.js")) { + ++gExpectedResourcesSeen; + } + if (gExpectedResourcesSeen == 2) { + browser.webRequest.onBeforeRequest.removeListener(onBeforeRequest); + browser.test.sendMessage("finish"); + } + return undefined; + } + + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, + { urls: ["*://*/*"] }, + ["blocking"] + ); + browser.test.sendMessage("ready"); + }, + }); + await extension.startup(); + await extension.awaitMessage("ready"); +}); + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", true], + // the test doesn't open a private window, so we don't care about this pref's value + ["privacy.trackingprotection.pbmode.enabled", false], + // tracking annotations aren't needed in this test, only TP is needed + ["privacy.trackingprotection.annotate_channels", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.trackingprotection.testing.report_blocked_node", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let promise = extension.awaitMessage("finish"); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await promise; + + info("Verify the number of tracking nodes found"); + await SpecialPowers.spawn(browser, [{ expected: 3 }], async function (obj) { + is( + content.document.blockedNodeByClassifierCount, + obj.expected, + "Expected tracking nodes found" + ); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + await extension.unload(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js new file mode 100644 index 0000000000..0674b87136 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_onModifyRequestNotificationForTrackingResources.js @@ -0,0 +1,96 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/** + * This test ensures that http-on-modify-request is dispatched for channels that + * are blocked by tracking protection. It sets up a page with a third-party script + * resource on it that is blocked by TP, and sets up an http-on-modify-request + * observer which waits to be notified about that resource. The test would time out + * if the http-on-modify-request notification isn't dispatched before the channel is + * canceled. + */ + +let gExpectedResourcesSeen = 0; +async function onModifyRequest() { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observer(subject, topic, data) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + let spec = httpChannel.URI.spec; + info("Observed channel for " + spec); + if (httpChannel.URI.prePath + "/" != TEST_3RD_PARTY_DOMAIN_TP) { + return; + } + if (spec.endsWith("empty.js")) { + ok(true, "Correct resource observed"); + ++gExpectedResourcesSeen; + } else if (spec.endsWith("empty.js?redirect")) { + httpChannel.redirectTo( + Services.io.newURI(spec.replace("empty.js?redirect", "head.js")) + ); + } else if (spec.endsWith("empty.js?redirect2")) { + httpChannel.suspend(); + setTimeout(() => { + httpChannel.redirectTo( + Services.io.newURI(spec.replace("empty.js?redirect2", "head.js")) + ); + httpChannel.resume(); + }, 100); + } else if (spec.endsWith("head.js")) { + ++gExpectedResourcesSeen; + } + if (gExpectedResourcesSeen == 3) { + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }, "http-on-modify-request"); + }); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", true], + // the test doesn't open a private window, so we don't care about this pref's value + ["privacy.trackingprotection.pbmode.enabled", false], + // tracking annotations aren't needed in this test, only TP is needed + ["privacy.trackingprotection.annotate_channels", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ["privacy.trackingprotection.testing.report_blocked_node", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let promise = onModifyRequest(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_EMBEDDER_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await promise; + + info("Verify the number of tracking nodes found"); + await SpecialPowers.spawn( + browser, + [{ expected: gExpectedResourcesSeen }], + async function (obj) { + is( + content.document.blockedNodeByClassifierCount, + obj.expected, + "Expected tracking nodes found" + ); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); 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" + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js new file mode 100644 index 0000000000..ce74076825 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedConsoleMessage.js @@ -0,0 +1,80 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Bug 1759496 - A test to verify if the console message of partitioned storage + * was sent correctly. + */ + +"use strict"; + +add_setup(async function () { + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + false + ); +}); + +add_task(async function runTest() { + info("Creating the tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let consolePromise = new Promise(resolve => { + let consoleListener = { + observe(msg) { + if ( + msg + .QueryInterface(Ci.nsIScriptError) + .category.startsWith("cookiePartitioned") + ) { + Services.console.unregisterListener(consoleListener); + resolve(msg.QueryInterface(Ci.nsIScriptError).errorMessage); + } + }, + }; + + Services.console.registerListener(consoleListener); + }); + + info("Creating the third-party iframe"); + let ifrBC = await SpecialPowers.spawn( + browser, + [TEST_TOP_PAGE_7], + async page => { + let ifr = content.document.createElement("iframe"); + + let loading = ContentTaskUtils.waitForEvent(ifr, "load"); + content.document.body.appendChild(ifr); + ifr.src = page; + await loading; + + return ifr.browsingContext; + } + ); + + info("Write cookie to the third-party iframe to ensure the console message"); + await SpecialPowers.spawn(ifrBC, [], async _ => { + content.document.cookie = "foo"; + }); + + let msg = await consolePromise; + + ok( + msg.startsWith("Partitioned cookie or storage access was provided to"), + "The partitioned console message was sent correctly" + ); + + info("Clean up"); + BrowserTestUtils.removeTab(tab); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js new file mode 100644 index 0000000000..d2d1e87dd4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedCookies.js @@ -0,0 +1,137 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "HTTP Cookies", + async (win3rdParty, win1stParty, allowed) => { + await win3rdParty.fetch("cookies.sjs?3rd").then(r => r.text()); + await win3rdParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie:foopy=3rd", "3rd party cookie set"); + }); + + await win1stParty.fetch("cookies.sjs?first").then(r => r.text()); + await win1stParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie:foopy=first", "First party cookie set"); + }); + + await win3rdParty + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + if (allowed) { + is( + text, + "cookie:foopy=first", + "3rd party has the first party cookie set" + ); + } else { + is( + text, + "cookie:foopy=3rd", + "3rd party has not the first party cookie set" + ); + } + }); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "DOM Cookies", + async (win3rdParty, win1stParty, allowed) => { + win3rdParty.document.cookie = "foo=3rd"; + is(win3rdParty.document.cookie, "foo=3rd", "3rd party cookie set"); + + win1stParty.document.cookie = "foo=first"; + is(win1stParty.document.cookie, "foo=first", "First party cookie set"); + + if (allowed) { + is( + win3rdParty.document.cookie, + "foo=first", + "3rd party has the first party cookie set" + ); + } else { + is( + win3rdParty.document.cookie, + "foo=3rd", + "3rd party has not the first party cookie set" + ); + } + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - DOM Cookies", + "cookies", + + // getDataCallback + async win => { + return win.document.cookie; + }, + + // addDataCallback + async (win, value) => { + win.document.cookie = value; + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + true +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - Network Cookies", + "cookies", + + // getDataCallback + async win => { + return win + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + return text.substring("cookie:foopy=".length); + }); + }, + + // addDataCallback + async (win, value) => { + await win.fetch("cookies.sjs?" + value).then(r => r.text()); + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + true +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js new file mode 100644 index 0000000000..93fc71b11e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedDOMCache.js @@ -0,0 +1,110 @@ +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win3rdParty.caches.open("wow").then( + _ => { + ok(allowed, "DOM Cache cannot be used!"); + }, + _ => { + ok(!allowed, "DOM Cache cannot be used!"); + } + ); + + await win1stParty.caches.open("wow").then( + _ => { + ok(true, "DOM Cache should be available"); + }, + _ => { + ok(false, "DOM Cache should be available"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [[APS_PREF, false]], + + { runInSecureContext: true } +); + +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win1stParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache should be available"); + await cache.add("/"); + }, + _ => { + ok(false, "DOM Cache should be available"); + } + ); + + await win3rdParty.caches.open("wow").then( + async cache => { + ok(true, "DOM Cache can be used!"); + is(undefined, await cache.match("/"), "DOM Cache is partitioned"); + }, + _ => { + ok(false, "DOM Cache cannot be used!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [[APS_PREF, true]], + + { runInSecureContext: true } +); + +// Test that DOM cache is not available in PBM. +PartitionedStorageHelper.runTest( + "DOMCache", + async (win3rdParty, win1stParty, allowed) => { + await win1stParty.caches.open("wow").then( + async cache => { + ok(false, "DOM Cache should not be available in PBM"); + }, + _ => { + ok(true, "DOM Cache should not be available in PBM"); + } + ); + + await win3rdParty.caches.open("wow").then( + async cache => { + ok(false, "DOM Cache should not be available in PBM"); + }, + _ => { + ok(true, "DOM Cache should not be available in PBM"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [], + + { runInSecureContext: true, runInPrivateWindow: true } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js new file mode 100644 index 0000000000..70dd0b03db --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedIndexedDB.js @@ -0,0 +1,95 @@ +PartitionedStorageHelper.runTest( + "IndexedDB", + async (win3rdParty, win1stParty, allowed) => { + await new Promise(resolve => { + let a = win1stParty.indexedDB.open("test", 1); + ok(!!a, "IDB should not be blocked in 1st party contexts"); + + a.onsuccess = e => { + let db = e.target.result; + is(db.objectStoreNames.length, 1, "We have 1 objectStore"); + is(db.objectStoreNames[0], "foobar", "We have 'foobar' objectStore"); + resolve(); + }; + + a.onupgradeneeded = e => { + let db = e.target.result; + is(db.objectStoreNames.length, 0, "We have 0 objectStores"); + db.createObjectStore("foobar", { keyPath: "test" }); + }; + }); + + await new Promise(resolve => { + let a = win3rdParty.indexedDB.open("test", 1); + ok(!!a, "IDB should not be blocked in 3rd party contexts"); + + a.onsuccess = e => { + let db = e.target.result; + + is(db.objectStoreNames.length, 0, "We have 0 objectStore"); + resolve(); + }; + }); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runPartitioningTest( + "Partitioned tabs - IndexedDB", + "indexeddb", + + // getDataCallback + async win => { + return new Promise(resolve => { + let a = win.indexedDB.open("test", 1); + + a.onupgradeneeded = e => { + let db = e.target.result; + db.createObjectStore("foobar", { keyPath: "id" }); + }; + + a.onsuccess = e => { + let db = e.target.result; + db.transaction("foobar").objectStore("foobar").get(1).onsuccess = + ee => { + resolve( + ee.target.result === undefined ? "" : ee.target.result.value + ); + }; + }; + }); + }, + + // addDataCallback + async (win, value) => { + return new Promise(resolve => { + let a = win.indexedDB.open("test", 1); + + a.onsuccess = e => { + let db = e.target.result; + db + .transaction("foobar", "readwrite") + .objectStore("foobar") + .put({ id: 1, value }).onsuccess = _ => { + resolve(true); + }; + }; + }); + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js new file mode 100644 index 0000000000..fe45970132 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage.js @@ -0,0 +1,115 @@ +AntiTracking.runTestInNormalAndPrivateMode( + "localStorage and Storage Access API", + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + is(localStorage.foo, "42", "The value matches"); + hasThrown = false; + } catch (e) { + is(e.name, "SecurityError", "We want a security error message."); + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage has been exposed correctly"); + + let prevLocalStorage; + if (!shouldThrow) { + prevLocalStorage = localStorage; + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + if (shouldThrow) { + try { + is(localStorage.foo, undefined, "Undefined value after."); + ok(false, "localStorage should not be available"); + } catch (e) { + ok(true, "localStorage should not be available"); + } + } else { + ok(localStorage != prevLocalStorage, "We have a new localStorage"); + is(localStorage.foo, undefined, "Undefined value after."); + + localStorage.foo = 42; + ok(true, "LocalStorage is still allowed"); + is(localStorage.foo, "42", "The value matches"); + } + }, + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + is(localStorage.foo, "42", "The value matches"); + + var prevLocalStorage = localStorage; + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + // For non-tracking windows, calling the API is a no-op + ok(localStorage == prevLocalStorage, "We have a new localStorage"); + is(localStorage.foo, "42", "The value matches"); + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,tracking.example.com", + ], + ], + false, + false +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - localStorage", + "localstorage", + + // getDataCallback + async win => { + return "foo" in win.localStorage ? win.localStorage.foo : ""; + }, + + // addDataCallback + async (win, value) => { + win.localStorage.foo = value; + return true; + }, + + // cleanup + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js new file mode 100644 index 0000000000..14ca62d062 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedLocalStorage_events.js @@ -0,0 +1,1014 @@ +function log(test) { + if ("iteration" in test) { + info( + `Running test with prefValue: ${test.prefValue} (Test #${ + test.iteration + 1 + })` + ); + test.iteration++; + } else { + test.iteration = 0; + log(test); + } +} + +function runAllTests(prefValue) { + const storagePrincipalTest = + prefValue == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + const dynamicFPITest = + prefValue == + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + + const test = { dynamicFPITest, prefValue }; + + let thirdPartyDomain; + if (storagePrincipalTest) { + thirdPartyDomain = TEST_3RD_PARTY_DOMAIN; + } + if (dynamicFPITest) { + thirdPartyDomain = TEST_4TH_PARTY_DOMAIN; + } + ok(thirdPartyDomain, "Sanity check"); + + // A same origin (and same-process via setting "dom.ipc.processCount" to 1) + // top-level window with access to real localStorage does not share storage + // with an ePartitionOrDeny iframe that should have PartitionedLocalStorage and + // no storage events are received in either direction. (Same-process in order + // to avoid having to worry about any e10s propagation issues.) + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("Creating a tracker top-level context"); + let trackerTab = BrowserTestUtils.addTab( + gBrowser, + thirdPartyDomain + TEST_PATH + "page.html" + ); + let trackerBrowser = gBrowser.getBrowserForTab(trackerTab); + await BrowserTestUtils.browserLoaded(trackerBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok( + value.startsWith("tracker-"), + "The value is correctly set by the tracker" + ); + } + ); + + info("The tracker page should not have received events"); + await SpecialPowers.spawn(trackerBrowser, [], async _ => { + is(content.localStorage.foo, undefined, "Undefined value!"); + content.localStorage.foo = "normal-" + Math.random(); + }); + + info("Let's see if non-tracker page has received events"); + await SpecialPowers.spawn(normalBrowser, [], async _ => { + let ifr = content.document.getElementById("ifr"); + + info("Getting the value..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok( + value.startsWith("tracker-"), + "The value is correctly set by the tracker" + ); + + info("Getting the events..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getEvents", "*"); + }); + + is(events, 0, "No events"); + }); + + BrowserTestUtils.removeTab(trackerTab); + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Two ePartitionOrDeny iframes in the same tab in the same origin see the + // same localStorage values but no storage events are received from each other + // if dFPI is disabled. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr1 = content.document.createElement("iframe"); + ifr1.setAttribute("id", "ifr1"); + ifr1.setAttribute("src", obj.page); + + info("Iframe 1 loading..."); + await new content.Promise(resolve => { + ifr1.onload = resolve; + content.document.body.appendChild(ifr1); + }); + + let ifr2 = content.document.createElement("iframe"); + ifr2.setAttribute("id", "ifr2"); + ifr2.setAttribute("src", obj.page); + + info("Iframe 2 loading..."); + await new content.Promise(resolve => { + ifr2.onload = resolve; + content.document.body.appendChild(ifr2); + }); + + info("Setting localStorage value in ifr1..."); + ifr1.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr1..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr1.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr1"); + + info("Getting the value from ifr2..."); + value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + ok( + value.startsWith("tracker-"), + "The value is correctly set in ifr2" + ); + } else { + is(value, null, "The value is not set in ifr2"); + } + + info("Getting the events received by ifr2..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getEvents", "*"); + }); + + if (obj.dynamicFPITest) { + is(events, 1, "one event"); + } else { + is(events, 0, "No events"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Same as the previous test but with a cookie behavior of BEHAVIOR_ACCEPT + // instead of BEHAVIOR_REJECT_TRACKER so the iframes get real, persistent + // localStorage instead of partitioned localStorage. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr1 = content.document.createElement("iframe"); + ifr1.setAttribute("id", "ifr1"); + ifr1.setAttribute("src", obj.page); + + info("Iframe 1 loading..."); + await new content.Promise(resolve => { + ifr1.onload = resolve; + content.document.body.appendChild(ifr1); + }); + + let ifr2 = content.document.createElement("iframe"); + ifr2.setAttribute("id", "ifr2"); + ifr2.setAttribute("src", obj.page); + + info("Iframe 2 loading..."); + await new content.Promise(resolve => { + ifr2.onload = resolve; + content.document.body.appendChild(ifr2); + }); + + info("Setting localStorage value in ifr1..."); + ifr1.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr1..."); + let value1 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr1.contentWindow.postMessage("getValue", "*"); + }); + + ok(value1.startsWith("tracker-"), "The value is correctly set in ifr1"); + + info("Getting the value from ifr2..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getValue", "*"); + }); + + is(value2, value1, "The values match"); + + info("Getting the events received by ifr2..."); + let events = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr2.contentWindow.postMessage("getEvents", "*"); + }); + + is(events, 1, "One event"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe navigated between two distinct pages on the same + // origin does not see the values stored by the previous iframe. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Navigate..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.setAttribute("src", obj.page + "?" + Math.random()); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + is(value, value2, "The value is received"); + } else { + is(value2, null, "The value is undefined"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Navigate..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.setAttribute("src", obj.page + "?" + Math.random()); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + is(value, value2, "The value is received"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe on the same origin that is navigated to itself + // via window.location.reload() or equivalent does not see the values stored + // by its previous self. + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + dynamicFPITest: test.dynamicFPITest, + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Reload..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.contentWindow.postMessage("reload", "*"); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + if (obj.dynamicFPITest) { + is(value, value2, "The value is equal"); + } else { + is(value2, null, "The value is undefined"); + } + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + + info("Reload..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + ifr.contentWindow.postMessage("reload", "*"); + }); + + info("Getting the value from ifr..."); + let value2 = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + is(value, value2, "The value is equal"); + } + ); + + BrowserTestUtils.removeTab(normalTab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // An ePartitionOrDeny iframe on different top-level domain tabs + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", prefValue], + ["network.cookie.cookieBehavior.pbmode", prefValue], + ["privacy.firstparty.isolate", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + let result1 = await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + return value; + } + ); + ok(result1.startsWith("tracker-"), "The value is correctly set in tab1"); + + info("Creating a non-tracker top-level context"); + let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2); + let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2); + await BrowserTestUtils.browserLoaded(normalBrowser2); + + info("The non-tracker page opens a tracker iframe"); + let result2 = await SpecialPowers.spawn( + normalBrowser2, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + return value; + } + ); + + ok(!result2, "The value is null"); + + BrowserTestUtils.removeTab(normalTab); + BrowserTestUtils.removeTab(normalTab2); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Like the previous test, but accepting trackers + add_task(async _ => { + log(test); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["privacy.firstparty.isolate", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.partitionedHosts", + "tracking.example.org,not-tracking.example.com", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let normalTab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let normalBrowser = gBrowser.getBrowserForTab(normalTab); + await BrowserTestUtils.browserLoaded(normalBrowser); + + info("The non-tracker page opens a tracker iframe"); + let result1 = await SpecialPowers.spawn( + normalBrowser, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Setting localStorage value in ifr..."); + ifr.contentWindow.postMessage("setValue", "*"); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + + ok(value.startsWith("tracker-"), "The value is correctly set in ifr"); + return value; + } + ); + ok(result1.startsWith("tracker-"), "The value is correctly set in tab1"); + + info("Creating a non-tracker top-level context"); + let normalTab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_2); + let normalBrowser2 = gBrowser.getBrowserForTab(normalTab2); + await BrowserTestUtils.browserLoaded(normalBrowser2); + + info("The non-tracker page opens a tracker iframe"); + let result2 = await SpecialPowers.spawn( + normalBrowser2, + [ + { + page: thirdPartyDomain + TEST_PATH + "localStorageEvents.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + info("Getting the value from ifr..."); + let value = await new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + ifr.contentWindow.postMessage("getValue", "*"); + }); + return value; + } + ); + + is(result1, result2, "The value is undefined"); + + BrowserTestUtils.removeTab(normalTab); + BrowserTestUtils.removeTab(normalTab2); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + // Cleanup data. + add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }); +} + +for (let pref of [ + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, +]) { + runAllTests(pref); +} diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js new file mode 100644 index 0000000000..683b1cc874 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedMessaging.js @@ -0,0 +1,20 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "BroadcastChannel", + async (win3rdParty, win1stParty, allowed) => { + let a = new win3rdParty.BroadcastChannel("hello"); + ok(!!a, "BroadcastChannel should be created by 3rd party iframe"); + + let b = new win1stParty.BroadcastChannel("hello"); + ok(!!b, "BroadcastChannel should be created by 1st party iframe"); + + // BroadcastChannel uses the incument global, this means that its CTOR will + // always use the 3rd party iframe's window as global. + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js new file mode 100644 index 0000000000..f45caba43a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js @@ -0,0 +1,625 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +PartitionedStorageHelper.runTest( + "ServiceWorkers - disable partitioning", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are disabled in third-party context. + await win3rdParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(allowed, "Success: ServiceWorker cannot be used!"); + }, + _ => { + ok(!allowed, "Failed: ServiceWorker cannot be used!"); + } + ); + + await win1stParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(true, "Success: ServiceWorker should be available!"); + }, + _ => { + ok(false, "Failed: ServiceWorker should be available!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", false], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - enable partitioning", + async (win3rdParty, win1stParty, allowed) => { + // Partitioned serviceWorkers are enabled in third-party context. + await win3rdParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok( + true, + "Success: ServiceWorker should be available in third parties." + ); + }, + _ => { + ok( + false, + "Failed: ServiceWorker should be available in third parties." + ); + } + ); + + await win1stParty.navigator.serviceWorker.register("empty.js").then( + _ => { + ok(true, "Success: ServiceWorker should be available!"); + }, + _ => { + ok(false, "Failed: ServiceWorker should be available!"); + } + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - MatchAll", + async (win3rdParty, win1stParty, allowed) => { + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker(win1stParty, "matchAll.js"); + } + + let msgPromise = new Promise(resolve => { + win1stParty.navigator.serviceWorker.addEventListener("message", msg => { + resolve(msg.data); + }); + }); + + win1stParty.sw.postMessage(win3rdParty.location.href); + let msg = await msgPromise; + + // The service worker will always be partitioned. So, the first party window + // won't have control on the third-party window. + is( + false, + msg, + "We won't have the 3rd party window controlled regardless of StorageAccess." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition ScriptContext", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set a script value to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetScriptValue", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set script value to first-party service worker."); + + // Set a script value to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetScriptValue", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set script value to third-party service worker."); + + // Get and check script value from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetScriptValue" } + ); + is( + res.value, + "1stParty", + "The script value in first party window is correct" + ); + + // Get and check script value from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetScriptValue" } + ); + + is( + res.value, + "3rdParty", + "The script value in third party window is correct" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition DOM Cache", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set DOM cache to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetCache", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set cache to first-party service worker."); + + // Set DOM cache to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetCache", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set cache to third-party service worker."); + + // Check DOM cache from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "HasCache", value: "1stParty" } + ); + ok( + res.value, + "The '1stParty' cache storage should exist for the first-party window." + ); + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "HasCache", value: "3rdParty" } + ); + ok( + !res.value, + "The '3rdParty' cache storage should not exist for the first-party window." + ); + + // Check DOM cache from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "HasCache", value: "1stParty" } + ); + + ok( + !res.value, + "The '1stParty' cache storage should not exist for the third-party window." + ); + + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "HasCache", value: "3rdParty" } + ); + + ok( + res.value, + "The '3rdParty' cache storage should exist for the third-party window." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition IndexedDB", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Set indexedDB value to first-party service worker. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { + type: "SetIndexedDB", + value: "1stParty", + } + ); + ok(res.result, "OK", "Set cache to first-party service worker."); + + // Set indexedDB value to third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { + type: "SetIndexedDB", + value: "3rdParty", + } + ); + ok(res.result, "OK", "Set cache to third-party service worker."); + + // Get and check indexedDB value from the first-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetIndexedDB" } + ); + is( + res.value, + "1stParty", + "The indexedDB value in first party window is correct" + ); + + // Get and check indexedDB from the third-party service worker. + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetIndexedDB" } + ); + + is( + res.value, + "3rdParty", + "The indexedDB value in third party window is correct" + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +PartitionedStorageHelper.runTest( + "ServiceWorkers - Partition Intercept", + async (win3rdParty, win1stParty, allowed) => { + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Fetch a resource in the first-party window. + await win1stParty.fetch("empty.js"); + + // Check that only the first-party service worker gets fetch event. + let res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "http://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js", + "The first-party service worker received fetch event." + ); + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "", + "The third-party service worker received no fetch event." + ); + + // Fetch a resource in the third-party window. + await win3rdParty.fetch("empty.js"); + + // Check if the fetch event only happens in third-party service worker. + res = await sendAndWaitWorkerMessage( + win1stParty.sw, + win1stParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "", + "The first-party service worker received no fetch event." + ); + res = await sendAndWaitWorkerMessage( + win3rdParty.sw, + win3rdParty.navigator.serviceWorker, + { type: "GetFetchURL" } + ); + is( + res.value, + "http://not-tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js", + "The third-party service worker received fetch event." + ); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +// Bug1743236 - Verify the content process won't crash if we create a dedicated +// worker in a service worker controlled third-party page with Storage Access. +PartitionedStorageHelper.runTest( + "ServiceWorkers - Create Dedicated Worker", + async (win3rdParty, win1stParty, allowed) => { + // We only do this test when the storage access is granted. + if (!allowed) { + return; + } + + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Create a dedicated worker in first-party window. + let firstPartyWorker = new win1stParty.Worker("dedicatedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + firstPartyWorker.addEventListener("message", msg => { + if (msg.data == "1stParty") { + resolve(); + } + }); + + firstPartyWorker.postMessage("1stParty"); + }); + + // Create a dedicated worker in third-party window. + let thirdPartyWorker = new win3rdParty.Worker("dedicatedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + thirdPartyWorker.addEventListener("message", msg => { + if (msg.data == "3rdParty") { + resolve(); + } + }); + + thirdPartyWorker.postMessage("3rdParty"); + }); + + firstPartyWorker.terminate(); + thirdPartyWorker.terminate(); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); + +// Bug1768193 - Verify the parent process won't crash if we create a shared +// worker in a service worker controlled third-party page with Storage Access. +PartitionedStorageHelper.runTest( + "ServiceWorkers - Create Shared Worker", + async (win3rdParty, win1stParty, allowed) => { + // We only do this test when the storage access is granted. + if (!allowed) { + return; + } + + // Register service worker for the first-party window. + if (!win1stParty.sw) { + win1stParty.sw = await registerServiceWorker( + win1stParty, + "serviceWorker.js" + ); + } + + // Register service worker for the third-party window. + if (!win3rdParty.sw) { + win3rdParty.sw = await registerServiceWorker( + win3rdParty, + "serviceWorker.js" + ); + } + + // Create a shared worker in third-party window. + let thirdPartyWorker = new win3rdParty.SharedWorker("sharedWorker.js"); + + // Post a message to the dedicated worker and wait until the message circles + // back. + await new Promise(resolve => { + thirdPartyWorker.port.onmessage = msg => { + resolve(); + }; + thirdPartyWorker.onerror = _ => { + ok(false, "We should not be here"); + resolve(); + }; + thirdPartyWorker.port.postMessage("count"); + }); + + thirdPartyWorker.port.postMessage("close"); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + + [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["privacy.partition.serviceWorkers", true], + ] +); diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js new file mode 100644 index 0000000000..337d36b6e4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_partitionedSharedWorkers.js @@ -0,0 +1,74 @@ +PartitionedStorageHelper.runTestInNormalAndPrivateMode( + "SharedWorkers", + async (win3rdParty, win1stParty, allowed) => { + // This test fails if run with an HTTPS 3rd-party URL because the shared worker + // which would start from the window opened from 3rdPartyStorage.html will become + // secure context and per step 11.4.3 of + // https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker attempting + // to run the SharedWorker constructor would emit an error event. + is( + win3rdParty.location.protocol, + "http:", + "Our 3rd party URL shouldn't be HTTPS" + ); + + let sh1 = new win1stParty.SharedWorker("sharedWorker.js"); + await new Promise(resolve => { + sh1.port.onmessage = e => { + is(e.data, 1, "We expected 1 connection"); + resolve(); + }; + sh1.port.postMessage("count"); + }); + + let sh3 = new win3rdParty.SharedWorker("sharedWorker.js"); + await new Promise(resolve => { + sh3.port.onmessage = e => { + is(e.data, 1, `We expected 1 connection for 3rd party SharedWorker`); + resolve(); + }; + sh3.onerror = _ => { + ok(false, "We should not be here"); + resolve(); + }; + sh3.port.postMessage("count"); + }); + + sh1.port.postMessage("close"); + sh3.port.postMessage("close"); + }, + + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } +); + +PartitionedStorageHelper.runPartitioningTestInNormalAndPrivateMode( + "Partitioned tabs - SharedWorker", + "sharedworker", + + // getDataCallback + async win => { + win.sh = new win.SharedWorker("partitionedSharedWorker.js"); + return new Promise(resolve => { + win.sh.port.onmessage = e => { + resolve(e.data); + }; + win.sh.port.postMessage({ what: "get" }); + }); + }, + + // addDataCallback + async (win, value) => { + win.sh = new win.SharedWorker("partitionedSharedWorker.js"); + win.sh.port.postMessage({ what: "put", value }); + return true; + }, + + // cleanup + async _ => {} +); diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js new file mode 100644 index 0000000000..a414e7363b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows.js @@ -0,0 +1,106 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in normal windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + // We load the test script in the parent process to check permissions. + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("go", _ => { + function ok(what, msg) { + sendAsyncMessage("ok", { what: !!what, msg }); + } + + function is(a, b, msg) { + ok(a === b, msg); + } + + // We should use the principal of the TEST_DOMAIN since the storage + // permission is saved under it. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + + sendAsyncMessage("done"); + }); + }); + + chromeScript.addMessageListener("ok", obj => { + ok(obj.what, obj.msg); + }); + + await new Promise(resolve => { + chromeScript.addMessageListener("done", _ => { + chromeScript.destroy(); + resolve(); + }); + + chromeScript.sendAsyncMessage("go"); + }); + + // We check the permission in tracking processes for non-Fission mode. In + // Fission mode, the permission won't be synced to the tracking process, + // so we don't check it. + if (!SpecialPowers.useRemoteSubframes) { + let Services = SpecialPowers.Services; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + 0, // don't expect blocking notifications + false +); // run in normal windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js new file mode 100644 index 0000000000..8a53782ba2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInNormalWindows_alwaysPartition.js @@ -0,0 +1,109 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in normal windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + // We load the test script in the parent process to check permissions. + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("go", _ => { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + function ok(what, msg) { + sendAsyncMessage("ok", { what: !!what, msg }); + } + + function is(a, b, msg) { + ok(a === b, msg); + } + + // We should use the principal of the TEST_DOMAIN since the storage + // permission is saved under it. + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + + sendAsyncMessage("done"); + }); + }); + + chromeScript.addMessageListener("ok", obj => { + ok(obj.what, obj.msg); + }); + + await new Promise(resolve => { + chromeScript.addMessageListener("done", _ => { + chromeScript.destroy(); + resolve(); + }); + + chromeScript.sendAsyncMessage("go"); + }); + + // We check the permission in tracking processes for non-Fission mode. In + // Fission mode, the permission won't be synced to the tracking process, + // so we don't check it. + if (!SpecialPowers.useRemoteSubframes) { + let Services = SpecialPowers.Services; + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + "http://example.net/" + ); + + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_TIME, + "Permission must expire at a specific time" + ); + ok(perm.expireTime > 0, "Permission must have a expiry time"); + } + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + [ + // expected blocking notifications + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL, + ], + false +); // run in normal windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js new file mode 100644 index 0000000000..8c8944fe2f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows.js @@ -0,0 +1,50 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in private windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + let Services = SpecialPowers.Services; + // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't + // accessible in the context of the web page... + let principal = SpecialPowers.wrap(document).nodePrincipal; + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_SESSION, + "Permission must expire at the end of session" + ); + is(perm.expireTime, 0, "Permission must have no expiry time"); + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + 0, // don't expect blocking notifications + true +); // run in private windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js new file mode 100644 index 0000000000..defb96004b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionInPrivateWindows_alwaysPartition.js @@ -0,0 +1,49 @@ +AntiTracking.runTest( + "Test whether we receive any persistent permissions in private windows", + // Blocking callback + async _ => { + // Nothing to do here! + }, + + // Non blocking callback + async _ => { + try { + let Services = SpecialPowers.Services; + // We would use TEST_3RD_PARTY_DOMAIN here, except that the variable isn't + // accessible in the context of the web page... + let principal = SpecialPowers.wrap(document).nodePrincipal; + for (let perm of Services.perms.getAllForPrincipal(principal)) { + // Ignore permissions other than storage access + if (!perm.type.startsWith("3rdPartyStorage^")) { + continue; + } + is( + perm.expireType, + Services.perms.EXPIRE_SESSION, + "Permission must expire at the end of session" + ); + is(perm.expireTime, 0, "Permission must have no expiry time"); + } + } catch (e) { + alert(e); + } + }, + + // Cleanup callback + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + true, // run the window.open() test + true, // run the user interaction test + [ + // expected blocking notifications + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL, + ], + true +); // run in private windows diff --git a/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js new file mode 100644 index 0000000000..35fd83bd31 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_permissionPropagation.js @@ -0,0 +1,425 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +/** + * This test makes sure the when we grant the storage permission, the + * permission is also propagated to iframes within the same agent cluster, + * but not to iframes in the other tabs. + */ + +async function createTab(topUrl, iframeCount, opener, params) { + let newTab; + let browser; + if (opener) { + let promise = BrowserTestUtils.waitForNewTab(gBrowser, topUrl); + await SpecialPowers.spawn(opener, [topUrl], function (url) { + content.window.open(url, "_blank"); + }); + newTab = await promise; + browser = gBrowser.getBrowserForTab(newTab); + } else { + newTab = BrowserTestUtils.addTab(gBrowser, topUrl); + + browser = gBrowser.getBrowserForTab(newTab); + await BrowserTestUtils.browserLoaded(browser); + } + + await SpecialPowers.spawn( + browser, + [params, iframeCount, createTrackerFrame.toString()], + async function (params, count, fn) { + // eslint-disable-next-line no-eval + let fnCreateTrackerFrame = eval(`(() => (${fn}))()`); + await fnCreateTrackerFrame(params, count, ifr => { + ifr.contentWindow.postMessage( + { callback: params.msg.blockingCallback }, + "*" + ); + }); + } + ); + + return Promise.resolve(newTab); +} + +async function createTrackerFrame(params, count, callback) { + let iframes = []; + for (var i = 0; i < count; i++) { + iframes[i] = content.document.createElement("iframe"); + await new content.Promise(resolve => { + iframes[i].id = "ifr" + i; + iframes[i].src = params.page; + iframes[i].onload = resolve; + content.document.body.appendChild(iframes[i]); + }); + + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + callback(iframes[i]); + }); + } +} + +async function testPermission(browser, block, params) { + await SpecialPowers.spawn( + browser, + [block, params], + async function (block, params) { + for (let i = 0; ; i++) { + let ifr = content.document.getElementById("ifr" + i); + if (!ifr) { + break; + } + + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + if (block) { + ifr.contentWindow.postMessage( + { callback: params.msg.blockingCallback }, + "*" + ); + } else { + ifr.contentWindow.postMessage( + { callback: params.msg.nonBlockingCallback }, + "*" + ); + } + }); + } + } + ); +} + +add_task(async function testPermissionGrantedOn3rdParty() { + info("Starting permission propagation test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + console.log("test hasStorageAccessInitially\n"); + await hasStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + let top = TEST_TOP_PAGE; + let page = TEST_3RD_PARTY_PAGE_WORKER; + let pageOther = + TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html"; + let params = { page, msg, pageOther }; + // Create 4 tabs: + // 1. The first tab has two tracker iframes, said A & B. + // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C. + // 3. The third tab has one tracker iframe, said D. + // 4. The fourth tab is opened by the first tab but with a different top-level url). + // The tab has one tracker iframe, said E. + // + // This test grants permission on iframe A, which then should propagate storage + // permission to iframe B & C, but not D, E + + info("Creating the first tab"); + let tab1 = await createTab(top, 2, null, params); + let browser1 = gBrowser.getBrowserForTab(tab1); + + info("Creating the second tab"); + let tab2 = await createTab(top, 1, browser1 /* opener */, params); + let browser2 = gBrowser.getBrowserForTab(tab2); + + info("Creating the third tab"); + let tab3 = await createTab(top, 1, null, params); + let browser3 = gBrowser.getBrowserForTab(tab3); + + info("Creating the fourth tab"); + let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params); + let browser4 = gBrowser.getBrowserForTab(tab4); + + info("Grant storage permission to the first iframe in the first tab"); + await SpecialPowers.spawn(browser1, [page, msg], async function (page, msg) { + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + let ifr = content.document.getElementById("ifr0"); + ifr.contentWindow.postMessage( + { + callback: (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + }).toString(), + }, + "*" + ); + }); + }); + + info("Both iframs of the first tab should have stroage permission"); + await testPermission(browser1, false /* block */, params); + + info("The iframe of the second tab should have storage permission"); + await testPermission(browser2, false /* block */, params); + + info("The iframe of the third tab should not have storage permission"); + await testPermission(browser3, true /* block */, params); + + info("The iframe of the fourth tab should not have storage permission"); + await testPermission(browser4, true /* block */, params); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +add_task(async function testPermissionGrantedOnFirstParty() { + info("Starting permission propagation test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + console.log("test hasStorageAccessInitially\n"); + await hasStorageAccessInitially(); + + await new Promise(resolve => { + // eslint-disable-next-line no-undef + let w = worker; + w.addEventListener( + "message", + e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, + { once: true } + ); + w.postMessage("go"); + }); + }).toString(); + + let top = TEST_TOP_PAGE; + let page = TEST_3RD_PARTY_PAGE_WORKER; + let params = { page, msg }; + // Create 4 tabs: + // 1. The first tab has two tracker iframes, said A & B. + // 2. The second tab is opened by the first tab, and it has one tracker iframe, said C. + // 3. The third tab has one tracker iframe, said D. + // 4. The fourth tab is opened by the first tab but with a different top-level url). + // The tab has one tracker iframe, said E. + // + // This test grants permission on iframe A, which then should propagate storage + // permission to iframe B & C, but not D, E + + info("Creating the first tab"); + let tab1 = await createTab(top, 2, null, params); + let browser1 = gBrowser.getBrowserForTab(tab1); + + info("Creating the second tab"); + let tab2 = await createTab(top, 1, browser1 /* opener */, params); + let browser2 = gBrowser.getBrowserForTab(tab2); + + info("Creating the third tab"); + let tab3 = await createTab(top, 1, null, params); + let browser3 = gBrowser.getBrowserForTab(tab3); + + info("Creating the fourth tab"); + let tab4 = await createTab(TEST_TOP_PAGE_2, 1, browser1, params); + let browser4 = gBrowser.getBrowserForTab(tab4); + + info("Grant storage permission to the first iframe in the first tab"); + let promise = BrowserTestUtils.waitForNewTab(gBrowser, page); + await SpecialPowers.spawn(browser1, [page], async function (page) { + content.window.open(page, "_blank"); + }); + let tab = await promise; + BrowserTestUtils.removeTab(tab); + + info("Both iframs of the first tab should have stroage permission"); + await testPermission(browser1, false /* block */, params); + + info("The iframe of the second tab should have storage permission"); + await testPermission(browser2, false /* block */, params); + + info("The iframe of the third tab should not have storage permission"); + await testPermission(browser3, true /* block */, params); + + info("The iframe of the fourth tab should not have storage permission"); + await testPermission(browser4, true /* block */, params); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js new file mode 100644 index 0000000000..e9030b98e4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_referrerDefaultPolicy.js @@ -0,0 +1,634 @@ +"use strict"; + +requestLongerTimeout(8); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/base/content/test/general/head.js", + this +); + +async function openAWindow(usePrivate) { + info("Creating a new " + (usePrivate ? "private" : "normal") + " window"); + let win = OpenBrowserWindow({ private: usePrivate }); + await TestUtils.topicObserved( + "browser-delayed-startup-finished", + subject => subject == win + ).then(() => win); + await BrowserTestUtils.firstBrowserLoaded(win); + return win; +} + +async function testOnWindowBody(win, expectedReferrer, rp) { + let browser = win.gBrowser; + let tab = browser.selectedTab; + let b = browser.getBrowserForTab(tab); + await promiseTabLoadEvent(tab, TEST_TOP_PAGE); + + info("Loading tracking scripts and tracking images"); + let referrer = await SpecialPowers.spawn( + b, + [{ rp }], + async function ({ rp }) { + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + if (rp) { + src.referrerPolicy = rp; + } + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=script"; + await p; + } + + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + if (rp) { + img.referrerPolicy = rp; + } + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=image"; + await p; + } + + { + let iframe = content.document.createElement("iframe"); + let p = new content.Promise(resolve => { + iframe.onload = resolve; + }); + content.document.body.appendChild(iframe); + if (rp) { + iframe.referrerPolicy = rp; + } + iframe.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=iframe"; + await p; + + p = new content.Promise(resolve => { + content.onmessage = event => { + resolve(event.data); + }; + }); + iframe.contentWindow.postMessage("ping", "*"); + return p; + } + } + ); + + is(referrer, expectedReferrer, "The correct referrer must be read from DOM"); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=iframe" + ) + .then(r => r.text()) + .then(text => { + is(text, expectedReferrer, "We sent the correct Referer header"); + }); +} + +async function closeAWindow(win) { + await BrowserTestUtils.closeWindow(win); +} + +let gRecording = true; +let gScenarios = []; +let gRPs = []; +let gTests = { private: [], nonPrivate: [] }; +const kPBPref = "network.http.referer.defaultPolicy.trackers.pbmode"; +const kNonPBPref = "network.http.referer.defaultPolicy.trackers"; + +function recordScenario(isPrivate, expectedReferrer, rp) { + if (!gRPs.includes(rp)) { + gRPs.push(rp); + } + gScenarios.push({ + private: isPrivate, + expectedReferrer, + rp, + pbPref: Services.prefs.getIntPref(kPBPref), + nonPBPref: Services.prefs.getIntPref(kNonPBPref), + }); +} + +async function testOnWindow(isPrivate, expectedReferrer, rp) { + if (gRecording) { + recordScenario(isPrivate, expectedReferrer, rp); + } +} + +function compileScenarios() { + let keys = { false: [], true: [] }; + for (let s of gScenarios) { + let key = { + rp: s.rp, + pbPref: s.pbPref, + nonPBPref: s.nonPBPref, + }; + let skip = false; + for (let k of keys[s.private]) { + if ( + key.rp == k.rp && + key.pbPref == k.pbPref && + key.nonPBPref == k.nonPBPref + ) { + skip = true; + break; + } + } + if (!skip) { + keys[s.private].push(key); + gTests[s.private ? "private" : "nonPrivate"].push({ + rp: s.rp, + pbPref: s.pbPref, + nonPBPref: s.nonPBPref, + expectedReferrer: s.expectedReferrer, + }); + } + } + + // Verify that all scenarios are checked + let counter = 1; + for (let s of gScenarios) { + let checked = false; + for (let tt in gTests) { + let isPrivate = tt == "private"; + for (let t of gTests[tt]) { + if ( + isPrivate == s.private && + t.rp == s.rp && + t.pbPref == s.pbPref && + t.nonPBPref == s.nonPBPref && + t.expectedReferrer == s.expectedReferrer + ) { + checked = true; + break; + } + } + } + ok(checked, `Scenario number ${counter++} checked`); + } +} + +async function executeTests() { + compileScenarios(); + + gRecording = false; + for (let mode in gTests) { + info(`Open a ${mode} window`); + while (gTests[mode].length) { + let test = gTests[mode].shift(); + info(`Running test ${test.toSource()}`); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.http.referer.defaultPolicy.trackers", test.nonPBPref], + ["network.http.referer.defaultPolicy.trackers.pbmode", test.pbPref], + ["dom.security.https_first_pbm", false], + ["network.http.referer.disallowCrossSiteRelaxingDefault", false], + ], + }); + + let win = await openAWindow(mode == "private"); + + await testOnWindowBody(win, test.expectedReferrer, test.rp); + + await closeAWindow(win); + } + } + + Services.prefs.clearUserPref(kPBPref); + Services.prefs.clearUserPref(kNonPBPref); +} + +function pn(name, isPrivate) { + return isPrivate ? name + ".pbmode" : name; +} + +async function testOnNoReferrer(isPrivate) { + // no-referrer pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // strict-origin-when-cross-origin pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // same-origin pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); + + // no-referrer pref when no-referrer is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, "", "no-referrer"); +} + +async function testOnSameOrigin(isPrivate) { + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // strict-origin-when-cross-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, "", "same-origin"); + + // same-origin pref when same-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, "", "same-origin"); +} + +async function testOnNoReferrerWhenDowngrade(isPrivate) { + // The setting referrer policy will be ignored if it is + // no-referrer-when-downgrade in private mode. It will fallback to the default + // value. + // + // The pref 'network.http.referer.disallowCrossSiteRelaxingDefault.pbmode' + // controls this behavior in private mode. + + // no-referrer-when-downgrade pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + + // strict-origin-when-cross-origin pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_DOMAIN, "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } + + // same-origin pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } + + // no-referrer pref when no-referrer-when-downgrade is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "no-referrer-when-downgrade"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "no-referrer-when-downgrade"); + } +} + +async function testOnOrigin(isPrivate) { + // origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // strict-origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // same-origin pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); + + // no-referrer pref when origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin"); +} + +async function testOnStrictOrigin(isPrivate) { + // strict-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // strict-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // same-origin pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); + + // no-referrer pref when strict-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin"); +} + +async function testOnOriginWhenCrossOrigin(isPrivate) { + // The setting referrer policy will be ignored if it is + // origin-when-cross-origin in private mode. It will fallback to the default + // value. The pref controls this behavior mentioned above. + + // no-referrer-when-downgrade pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } + + // strict-origin-when-cross-origin pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + + // same-origin pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } + + // no-referrer pref when origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "origin-when-cross-origin"); + } else { + await testOnWindow(isPrivate, TEST_DOMAIN, "origin-when-cross-origin"); + } +} + +async function testOnStrictOriginWhenCrossOrigin(isPrivate) { + // origin-when-cross-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // strict-origin-when-cross-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // same-origin pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); + + // no-referrer pref when strict-origin-when-cross-origin is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + await testOnWindow(isPrivate, TEST_DOMAIN, "strict-origin-when-cross-origin"); +} + +async function testOnUnsafeUrl(isPrivate) { + // The setting referrer policy will be ignored if it is unsafe in private + // mode. It will fallback to the default value. The pref controls this + // behavior mentioned above. + + // no-referrer-when-downgrade pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 3]], + }); + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + + // strict-origin-when-cross-origin pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 2]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, TEST_DOMAIN, "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } + + // same-origin pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 1]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } + + // no-referrer pref when unsafe-url is forced + await SpecialPowers.pushPrefEnv({ + set: [[pn("network.http.referer.defaultPolicy.trackers", isPrivate), 0]], + }); + if (isPrivate) { + await testOnWindow(isPrivate, "", "unsafe-url"); + } else { + await testOnWindow(isPrivate, TEST_TOP_PAGE, "unsafe-url"); + } +} + +add_task(async function () { + info("Starting referrer default policy test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["network.http.referer.defaultPolicy", 3], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + // no-referrer-when-downgrade + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 3]], + }); + await testOnWindow(false, TEST_TOP_PAGE, null); + + // strict-origin-when-cross-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 2]], + }); + await testOnWindow(false, TEST_DOMAIN, null); + + // same-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 1]], + }); + await testOnWindow(false, "", null); + + // no-referrer + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers", 0]], + }); + await testOnWindow(false, "", null); + + // override with no-referrer + await testOnNoReferrer(false); + + // override with same-origin + await testOnSameOrigin(false); + + // override with no-referrer-when-downgrade + await testOnNoReferrerWhenDowngrade(false); + + // override with origin + await testOnOrigin(false); + + // override with strict-origin + await testOnStrictOrigin(false); + + // override with origin-when-cross-origin + await testOnOriginWhenCrossOrigin(false); + + // override with strict-origin-when-cross-origin + await testOnStrictOriginWhenCrossOrigin(false); + + // override with unsafe-url + await testOnUnsafeUrl(false); + + // Reset the pref. + Services.prefs.clearUserPref("network.http.referer.defaultPolicy.trackers"); + + // no-referrer-when-downgrade + await SpecialPowers.pushPrefEnv({ + set: [ + // Set both prefs, because if we only set the trackers pref, then the PB + // mode default policy pref (2) would apply! + ["network.http.referer.defaultPolicy.pbmode", 3], + ["network.http.referer.defaultPolicy.trackers.pbmode", 3], + ], + }); + await testOnWindow(true, TEST_TOP_PAGE, null); + + // strict-origin-when-cross-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 2]], + }); + await testOnWindow(true, TEST_DOMAIN, null); + + // same-origin + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 1]], + }); + await testOnWindow(true, "", null); + + // no-referrer + await SpecialPowers.pushPrefEnv({ + set: [["network.http.referer.defaultPolicy.trackers.pbmode", 0]], + }); + await testOnWindow(true, "", null); + + // override with no-referrer + await testOnNoReferrer(true); + + // override with same-origin + await testOnSameOrigin(true); + + // override with no-referrer-when-downgrade + await testOnNoReferrerWhenDowngrade(true); + + // override with origin + await testOnOrigin(true); + + // override with strict-origin + await testOnStrictOrigin(true); + + // override with origin-when-cross-origin + await testOnOriginWhenCrossOrigin(true); + + // override with strict-origin-when-cross-origin + await testOnStrictOriginWhenCrossOrigin(true); + + // override with unsafe-url + await testOnUnsafeUrl(true); + + // Reset the pref. + Services.prefs.clearUserPref( + "network.http.referer.defaultPolicy.trackers.pbmode" + ); +}); + +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + await executeTests(); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_script.js b/toolkit/components/antitracking/test/browser/browser_script.js new file mode 100644 index 0000000000..ef6c7f67c6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_script.js @@ -0,0 +1,224 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js", + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Checking if permission is denied"); + let callbackBlocked = async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }; + + let assertBlocked = () => + new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackBlocked.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + await assertBlocked(); + + info("Triggering a 3rd party script..."); + let p = new content.Promise(resolve => { + let bc = new content.BroadcastChannel("a"); + bc.onmessage = resolve; + }); + + let src = content.document.createElement("script"); + content.document.body.appendChild(src); + src.src = obj.scriptURL; + + await p; + + info("Checking if permission is denied before interacting with tracker"); + await assertBlocked(); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + scriptURL: TEST_DOMAIN + TEST_PATH + "tracker.js", + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Checking if permission is denied"); + let callbackBlocked = async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }; + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackBlocked.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + + info("Triggering a 3rd party script..."); + let p = new content.Promise(resolve => { + let bc = new content.BroadcastChannel("a"); + bc.onmessage = resolve; + }); + + let src = content.document.createElement("script"); + content.document.body.appendChild(src); + src.src = obj.scriptURL; + + await p; + + info("Checking if permission is granted"); + let callbackAllowed = async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }; + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(callbackAllowed.toString(), "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js new file mode 100644 index 0000000000..4ae4ba74dd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_serviceWorkersWithStorageAccessGranted.js @@ -0,0 +1,148 @@ +/** This tests that the service worker can be used if we have storage access + * permission. We manually write the storage access permission into the + * permission manager to simulate the storage access has been granted. We would + * test the service worker three times. The fist time is to check the service + * work is allowed. The second time is to load again and check it won't hit + * assertion, this assertion would only be hit if we have registered a service + * worker, see Bug 1631234. + * + * The third time is to load again but in a sandbox iframe to check it won't + * hit the assertion. See Bug 1637226 for details. + * + * The fourth time is to load again in a nested iframe to check it won't hit + * the assertion. See Bug 1641153 for details. + * */ + +add_task(async _ => { + // Manually add the storage permission. + PermissionTestUtils.add( + TEST_DOMAIN, + "3rdPartyStorage^https://tracking.example.org", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); + + AntiTracking._createTask({ + name: "Test that we can use service worker if we have the storage access permission", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used!"); + }, + _ => { + ok(false, "ServiceWorker can be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + }); + + AntiTracking._createTask({ + name: "Test again to check if we can still use service worker without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used!"); + }, + _ => { + ok(false, "ServiceWorker can be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + }); + + AntiTracking._createTask({ + name: "Test again to check if we cannot use service worker in a sandbox iframe without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used in sandbox iframe!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used in sandbox iframe!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications, + runInPrivateWindow: false, + iframeSandbox: "allow-scripts allow-same-origin", + accessRemoval: null, + callbackAfterRemoval: null, + }); + + const NESTED_THIRD_PARTY_PAGE = + TEST_DOMAIN + TEST_PATH + "3rdPartyRelay.html?" + TEST_3RD_PARTY_PAGE; + + AntiTracking._createTask({ + name: "Test again to check if we can use service worker in a nested iframe without hit the assertion.", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + allowList: false, + callback: async _ => { + await navigator.serviceWorker + .register("empty.js") + .then( + _ => { + ok(true, "ServiceWorker can be used in nested iframe!"); + }, + _ => { + ok(false, "ServiceWorker can be used in nested iframe!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + extraPrefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: NESTED_THIRD_PARTY_PAGE, + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js new file mode 100644 index 0000000000..b35fbea8c7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_siteSpecificWorkArounds.js @@ -0,0 +1,153 @@ +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a pref", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage is allowed"); + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"]], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a fancy pref", + async _ => { + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior" + ); + + let shouldThrow = [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT, + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + ].includes(effectiveCookieBehavior); + + let hasThrown; + try { + localStorage.foo = 42; + hasThrown = false; + } catch (e) { + hasThrown = true; + } + + is(hasThrown, shouldThrow, "LocalStorage is allowed"); + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "urlclassifier.trackingAnnotationSkipURLs", + "foobar.example,*.example.org,baz.example", + ], + ], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a misconfigured pref", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["urlclassifier.trackingAnnotationSkipURLs", "*.tracking.example.org"]], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window + +AntiTracking.runTest( + "localStorage with a tracker that is entitylisted via a pref, but skip lists are disabled.", + async _ => { + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + async _ => { + localStorage.foo = 42; + ok(true, "LocalStorage is allowed"); + }, + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["urlclassifier.trackingAnnotationSkipURLs", "TRACKING.EXAMPLE.ORG"], + ["privacy.antitracking.enableWebcompat", false], + ], + false, // run the window.open() test + false, // run the user interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false +); // run in a normal window diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking.js b/toolkit/components/antitracking/test/browser/browser_socialtracking.js new file mode 100644 index 0000000000..1cbf5d9278 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_socialtracking.js @@ -0,0 +1,147 @@ +function runTest(obj) { + add_task(async _ => { + info("Test: " + obj.testName); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 1], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.trackingprotection.socialtracking.enabled", + obj.protectionEnabled, + ], + ["privacy.socialtracking.block_cookies.enabled", obj.cookieBlocking], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a non-tracker top-level context"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("The non-tracker page opens a tracker iframe"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "localStorage.html", + image: TEST_3RD_PARTY_DOMAIN_STP + TEST_PATH + "raptor.jpg", + loading: obj.loading, + result: obj.result, + }, + ], + async obj => { + let loading = await new content.Promise(resolve => { + let image = new content.Image(); + image.src = obj.image + "?" + Math.random(); + image.onload = _ => resolve(true); + image.onerror = _ => resolve(false); + }); + + is(loading, obj.loading, "Loading expected"); + + if (obj.loading) { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "ifr"); + ifr.setAttribute("src", obj.page); + + info("Iframe loading..."); + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + }); + + let p = new Promise(resolve => { + content.addEventListener( + "message", + e => { + resolve(e.data); + }, + { once: true } + ); + }); + + info("Setting localStorage value..."); + ifr.contentWindow.postMessage("test", "*"); + + info("Getting the value..."); + let value = await p; + is(value.status, obj.result, "We expect to succeed"); + } + } + ); + + info("Checking content blocking log."); + let contentBlockingLog = JSON.parse(await browser.getContentBlockingLog()); + let origins = Object.keys(contentBlockingLog); + is(origins.length, 1, "There should be one origin entry in the log."); + for (let origin of origins) { + is( + origin + "/", + TEST_3RD_PARTY_DOMAIN_STP, + "Correct tracker origin must be reported" + ); + Assert.deepEqual( + contentBlockingLog[origin], + obj.expectedLogItems, + "Content blocking log should be as expected" + ); + } + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +} + +runTest({ + testName: + "Socialtracking-annotation feature enabled but not considered for tracking detection.", + protectionEnabled: false, + loading: true, + cookieBlocking: false, + result: true, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], +}); + +runTest({ + testName: + "Socialtracking-annotation feature enabled and considered for tracking detection.", + protectionEnabled: false, + loading: true, + cookieBlocking: true, + result: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + [Ci.nsIWebProgressListener.STATE_LOADED_SOCIALTRACKING_CONTENT, true, 2], + // We cache the storage allowed decision, so we will only get one block + // event per window and origin. + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], +}); + +runTest({ + testName: "Socialtracking-protection feature enabled.", + protectionEnabled: true, + loading: false, + cookieBlocking: true, + result: false, + expectedLogItems: [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], +}); diff --git a/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js new file mode 100644 index 0000000000..e7389b1456 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js @@ -0,0 +1,114 @@ +/** + * Bug 1663992 - Testing the 'Save Image As' works in an image document if the + * image is blocked by social tracker. + */ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +const TEST_IMAGE_URL = + "http://social-tracking.example.org/browser/toolkit/components/antitracking/test/browser/raptor.jpg"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForTransferComplete() { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function (downloadSuccess) { + ok(downloadSuccess, "Image should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +add_setup(async function () { + info("Setting up the prefs."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.socialtracking.enabled", true], + [ + "urlclassifier.features.socialtracking.blacklistHosts", + "social-tracking.example.org", + ], + ], + }); + + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +add_task(async function () { + info("Open a new tab for testing"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_IMAGE_URL + ); + + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + + let browser = gBrowser.selectedBrowser; + + info("Open the context menu."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "img", + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let transferCompletePromise = createPromiseForTransferComplete(); + let saveElement = document.getElementById(`context-saveimage`); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js new file mode 100644 index 0000000000..5c232fabc2 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_PAGE = + "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_ANOTHER_PAGE = + "http://example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_PREFLIGHT_IFRAME_PAGE = + "http://mochi.test:8888/browser/toolkit/components/antitracking/test/browser/empty.html"; +const TEST_PREFLIGHT_PAGE = + "http://example.net/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs"; + +add_task(async function () { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // First, create one tab under one first party. + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGE + ); + let token = uuidGenerator.generateUUID().toString(); + + // Use fetch to verify that preflight cache is working. The preflight + // cache is keyed by the loading principal and the url. So, we load an + // iframe with one origin and use fetch in there to ensure we will have + // the same loading principal. + await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_PREFLIGHT_PAGE, TEST_PREFLIGHT_IFRAME_PAGE, token], + async (url, iframe_url, token) => { + let iframe = content.document.createElement("iframe"); + + await new Promise(resolve => { + iframe.onload = () => { + resolve(); + }; + content.document.body.appendChild(iframe); + iframe.src = iframe_url; + }); + + await SpecialPowers.spawn( + iframe, + [url, token], + async (url, token) => { + const test_url = `${url}?token=${token}`; + let response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + is( + await response.text(), + "1", + "The preflight should be sent at first time" + ); + + response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + is( + await response.text(), + "0", + "The preflight shouldn't be sent due to the preflight cache" + ); + } + ); + } + ); + + // Load the tab with a different first party. And use fetch to check if + // the preflight cache is partitioned. The fetch will also be performed in + // the iframe with the same origin as above to ensure we use the same + // loading principal. + BrowserTestUtils.loadURIString(tab.linkedBrowser, TEST_ANOTHER_PAGE); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [ + TEST_PREFLIGHT_PAGE, + TEST_PREFLIGHT_IFRAME_PAGE, + token, + networkIsolation, + ], + async (url, iframe_url, token, partitioned) => { + let iframe = content.document.createElement("iframe"); + + await new Promise(resolve => { + iframe.onload = () => { + resolve(); + }; + content.document.body.appendChild(iframe); + iframe.src = iframe_url; + }); + + await SpecialPowers.spawn( + iframe, + [url, token, partitioned], + async (url, token, partitioned) => { + const test_url = `${url}?token=${token}`; + + let response = await content.fetch( + new content.Request(test_url, { + mode: "cors", + method: "GET", + headers: [["x-test-header", "check"]], + }) + ); + + if (partitioned) { + is( + await response.text(), + "1", + "The preflight cache should be partitioned" + ); + } else { + is( + await response.text(), + "0", + "The preflight cache shouldn't be partitioned" + ); + } + } + ); + } + ); + + BrowserTestUtils.removeTab(tab); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs new file mode 100644 index 0000000000..0f7041aaa7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_CORS_preflight.sjs @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Cu.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + let query = new URLSearchParams(request.queryString); + let token = query.get("token"); + + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Headers", "x-test-header", false); + + if (request.method == "OPTIONS") { + response.setHeader( + "Access-Control-Allow-Methods", + request.getHeader("Access-Control-Request-Method"), + false + ); + response.setHeader("Access-Control-Max-Age", "20", false); + + setState(token, token); + } else { + let test_op = request.getHeader("x-test-header"); + + if (test_op == "check") { + let value = getState(token); + + if (value) { + response.write("1"); + setState(token, ""); + } else { + response.write("0"); + } + } + } +} diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js new file mode 100644 index 0000000000..6bacac0af9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var unsecureEmptyURL = + "http://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureEmptyURL = + "https://example.org/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureAnotherEmptyURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs"; +var unsecureURL = + "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs"; +var secureImgURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image"; +var unsecureImgURL = + "http://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?image"; +var secureIncludeSubURL = + "https://example.com/browser/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs?includeSub"; +var unsecureSubEmptyURL = + "http://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var secureSubEmptyURL = + "https://test1.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; +var unsecureNoCertSubEmptyURL = + "http://nocert.example.com/browser/toolkit/components/antitracking/test/browser/empty.html"; + +function cleanupHSTS(aPartitionEnabled, aUseSite) { + // Ensure to remove example.com from the HSTS list. + let sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + + for (let origin of ["example.com", "example.org"]) { + let originAttributes = {}; + + if (aPartitionEnabled) { + if (aUseSite) { + originAttributes = { partitionKey: `(http,${origin})` }; + } else { + originAttributes = { partitionKey: origin }; + } + } + + sss.resetState(NetUtil.newURI("http://example.com/"), originAttributes); + } +} + +function promiseTabLoadEvent(aTab, aURL, aFinalURL) { + info("Wait for load tab event"); + BrowserTestUtils.loadURIString(aTab.linkedBrowser, aURL); + return BrowserTestUtils.browserLoaded(aTab.linkedBrowser, false, aFinalURL); +} + +function waitFor(host, type) { + return new Promise(resolve => { + const observer = channel => { + if ( + channel instanceof Ci.nsIHttpChannel && + channel.URI.host === host && + channel.loadInfo.internalContentPolicyType === type + ) { + Services.obs.removeObserver(observer, "http-on-stop-request"); + resolve(channel.URI.spec); + } + }; + Services.obs.addObserver(observer, "http-on-stop-request"); + }); +} + +add_task(async function () { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Let's load the secureURL as first-party in order to activate HSTS. + await promiseTabLoadEvent(tab, secureURL, secureURL); + + // Let's test HSTS: unsecure -> secure. + await promiseTabLoadEvent(tab, unsecureURL, secureURL); + ok(true, "unsecure -> secure, first-party works!"); + + // Let's load a first-party. + await promiseTabLoadEvent(tab, unsecureEmptyURL, unsecureEmptyURL); + + let finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IFRAME + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [unsecureURL], async url => { + let ifr = content.document.createElement("iframe"); + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + if (networkIsolation) { + is(await finalURL, unsecureURL, "HSTS doesn't work for 3rd parties"); + } else { + is(await finalURL, secureURL, "HSTS works for 3rd parties"); + } + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); + +add_task(async function test_subresource() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Load a secure page as first party. + await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL); + + let loadPromise = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load a secure subresource. HSTS won't be activated, since third + // parties can't set HSTS. + await SpecialPowers.spawn( + tab.linkedBrowser, + [secureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + // Ensure the subresource is loaded. + await loadPromise; + + // Reload the secure page as first party. + await promiseTabLoadEvent(tab, secureEmptyURL, secureEmptyURL); + + let finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load an unsecure subresource. It should not be upgraded to https. + await SpecialPowers.spawn( + tab.linkedBrowser, + [unsecureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties"); + + // Load the secure page with a different origin as first party. + await promiseTabLoadEvent( + tab, + secureAnotherEmptyURL, + secureAnotherEmptyURL + ); + + finalURL = waitFor( + "example.com", + Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE + ); + + // Load a unsecure subresource + await SpecialPowers.spawn( + tab.linkedBrowser, + [unsecureImgURL], + async url => { + let ifr = content.document.createElement("img"); + content.document.body.appendChild(ifr); + ifr.src = url; + } + ); + + is(await finalURL, unsecureImgURL, "HSTS isn't set for 3rd parties"); + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); + +add_task(async function test_includeSubDomains() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ["security.mixed_content.upgrade_display_content", false], + ], + }); + + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + + // Load a secure page as first party to activate HSTS. + await promiseTabLoadEvent(tab, secureIncludeSubURL, secureIncludeSubURL); + + // Load a unsecure sub-domain page as first party to see if it's upgraded. + await promiseTabLoadEvent(tab, unsecureSubEmptyURL, secureSubEmptyURL); + + // Load a sub domain page which will trigger the cert error page. + let certErrorLoaded = BrowserTestUtils.waitForErrorPage( + tab.linkedBrowser + ); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + unsecureNoCertSubEmptyURL + ); + await certErrorLoaded; + + // Verify the error page has the 'badStsCert' in its query string + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let searchParams = new content.URLSearchParams( + content.document.documentURI + ); + + is( + searchParams.get("s"), + "badStsCert", + "The cert error page has 'badStsCert' set" + ); + }); + + gBrowser.removeCurrentTab(); + cleanupHSTS(networkIsolation, partitionPerSite); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs new file mode 100644 index 0000000000..185c7c7e05 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_HSTS.sjs @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" + + "DUlEQVQImWNgY2P7DwABOgESJhRQtgAAAABJRU5ErkJggg==" +); + +const PAGE = "<!DOCTYPE html><html><body><p>HSTS page</p></body></html>"; + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, "200", "OK"); + if (request.queryString == "includeSub") { + response.setHeader( + "Strict-Transport-Security", + "max-age=60; includeSubDomains" + ); + } else { + response.setHeader("Strict-Transport-Security", "max-age=60"); + } + + if (request.queryString == "image") { + response.setHeader("Content-Type", "image/png", false); + response.setHeader("Content-Length", IMG_BYTES.length + "", false); + response.write(IMG_BYTES); + return; + } + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Length", PAGE.length + "", false); + response.write(PAGE); +} diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js new file mode 100644 index 0000000000..f6bc072b75 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_cache.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const cacheURL = + "http://example.org/browser/browser/components/originattributes/test/browser/file_cache.html"; + +function countMatchingCacheEntries(cacheEntries, domain, fileSuffix) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes("file_thirdPartyChild." + fileSuffix)).length; +} + +async function checkCache(suffixes, originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + for (let suffix of suffixes) { + let foundEntryCount = countMatchingCacheEntries( + data, + "example.net", + suffix + ); + ok( + foundEntryCount > 0, + `Cache entries expected for ${suffix} and OA=${JSON.stringify( + originAttributes + )}` + ); + } +} + +add_task(async function () { + info("Disable predictor and accept all"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.predictor.enabled", false], + ["network.predictor.enable-prefetch", false], + ["network.cookie.cookieBehavior", 0], + ], + }); + + const tests = [ + { + prefValue: true, + originAttributes: { partitionKey: "(http,example.org)" }, + }, + { + prefValue: false, + originAttributes: {}, + }, + ]; + + for (let test of tests) { + info("Clear image and network caches"); + let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService( + SpecialPowers.Ci.imgITools + ); + let imageCache = tools.getImgCacheForDocument(window.document); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content + Services.cache2.clear(); + + info("Enabling network state partitioning"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", test.prefValue]], + }); + + info("Let's load a page to populate some entries"); + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser)); + BrowserTestUtils.loadURIString(tab.linkedBrowser, cacheURL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, cacheURL); + + let argObj = { + randomSuffix: Math.random(), + urlPrefix: + "http://example.net/browser/browser/components/originattributes/test/browser/", + }; + + await SpecialPowers.spawn( + tab.linkedBrowser, + [argObj], + async function (arg) { + // The CSS cache needs to be cleared in-process. + content.windowUtils.clearSharedStyleSheetCache(); + + let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv"; + let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg"; + let URLSuffix = "?r=" + arg.randomSuffix; + + // Create the audio and video elements. + let audio = content.document.createElement("audio"); + let video = content.document.createElement("video"); + let audioSource = content.document.createElement("source"); + + // Append the audio element into the body, and wait until they're finished. + await new content.Promise(resolve => { + let audioLoaded = false; + + let audioListener = () => { + Assert.ok(true, `Audio suspended: ${audioURL + URLSuffix}`); + audio.removeEventListener("suspend", audioListener); + + audioLoaded = true; + if (audioLoaded) { + resolve(); + } + }; + + Assert.ok(true, `Loading audio: ${audioURL + URLSuffix}`); + + // Add the event listeners before everything in case we lose events. + audio.addEventListener("suspend", audioListener); + + // Assign attributes for the audio element. + audioSource.setAttribute("src", audioURL + URLSuffix); + audioSource.setAttribute("type", "audio/ogg"); + + audio.appendChild(audioSource); + audio.autoplay = true; + + content.document.body.appendChild(audio); + }); + + // Append the video element into the body, and wait until it's finished. + await new content.Promise(resolve => { + let listener = () => { + Assert.ok(true, `Video suspended: ${videoURL + URLSuffix}`); + video.removeEventListener("suspend", listener); + resolve(); + }; + + Assert.ok(true, `Loading video: ${videoURL + URLSuffix}`); + + // Add the event listener before everything in case we lose the event. + video.addEventListener("suspend", listener); + + // Assign attributes for the video element. + video.setAttribute("src", videoURL + URLSuffix); + video.setAttribute("type", "video/ogg"); + + content.document.body.appendChild(video); + }); + } + ); + + let maybePartitionedSuffixes = [ + "iframe.html", + "link.css", + "script.js", + "img.png", + "favicon.png", + "object.png", + "embed.png", + "xhr.html", + "worker.xhr.html", + "audio.ogg", + "video.ogv", + "fetch.html", + "worker.fetch.html", + "request.html", + "worker.request.html", + "import.js", + "worker.js", + "sharedworker.js", + ]; + + info("Query the cache (maybe) partitioned cache"); + await checkCache(maybePartitionedSuffixes, test.originAttributes); + + gBrowser.removeCurrentTab(); + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js new file mode 100644 index 0000000000..a28f7d5adc --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_network.js @@ -0,0 +1,116 @@ +function altSvcCacheKeyIsolated(parsed) { + return parsed.length > 5 && parsed[5] == "I"; +} + +function altSvcPartitionKey(key) { + let parts = key.split(":"); + return parts[parts.length - 2]; +} + +const gHttpHandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler +); + +add_task(async function () { + info("Starting tlsSessionTickets test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_ACCEPT], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_ACCEPT, + ], + ["network.http.altsvc.proxy_checks", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", false], + ["privacy.partition.network_state", true], + ["privacy.partition.network_state.connection_with_proxy", true], + ], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + const thirdPartyURL = + "https://tlsresumptiontest.example.org/browser/toolkit/components/antitracking/test/browser/empty-altsvc.js"; + const partitionKey1 = "^partitionKey=%28http%2Cexample.net%29"; + const partitionKey2 = "^partitionKey=%28http%2Cmochi.test%29"; + + function checkAltSvcCache(keys) { + let arr = gHttpHandler.altSvcCacheKeys; + is( + arr.length, + keys.length, + "Found the expected number of items in the cache" + ); + for (let i = 0; i < arr.length; ++i) { + is( + altSvcPartitionKey(arr[i]), + keys[i], + "Expected top window origin found in the Alt-Svc cache key" + ); + } + } + + checkAltSvcCache([]); + + info("Loading something in the tab"); + await SpecialPowers.spawn(browser, [{ thirdPartyURL }], async function (obj) { + dump("AAA: " + content.window.location.href + "\n"); + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = obj.thirdPartyURL; + await p; + }); + + checkAltSvcCache([partitionKey1]); + + info("Creating a second tab"); + let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE_6); + gBrowser.selectedTab = tab2; + + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + info("Loading something in the second tab"); + await SpecialPowers.spawn( + browser2, + [{ thirdPartyURL }], + async function (obj) { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = obj.thirdPartyURL; + await p; + } + ); + + checkAltSvcCache([partitionKey1, partitionKey2]); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js new file mode 100644 index 0000000000..22db484e16 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_saveAs.js @@ -0,0 +1,532 @@ +/** + * Bug 1641270 - A test case for ensuring the save channel will use the correct + * cookieJarSettings when doing the saving and the cache would + * work as expected. + */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +const TEST_IMAGE_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsImage.sjs"; +const TEST_VIDEO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsVideo.sjs"; +const TEST_PAGEINFO_URL = TEST_DOMAIN + TEST_PATH + "file_saveAsPageInfo.html"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForTransferComplete(aDesirableFileName) { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + if (aDesirableFileName) { + is(fileName, aDesirableFileName, "The default file name is correct."); + } + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function (downloadSuccess) { + ok(downloadSuccess, "File should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +function createPromiseForObservingChannel(aURL, aPartitionKey) { + return new Promise(resolve => { + let observer = (aSubject, aTopic) => { + if (aTopic === "http-on-modify-request") { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + let reqLoadInfo = httpChannel.loadInfo; + + // Make sure this is the request which we want to check. + if (!httpChannel.URI.spec.endsWith(aURL)) { + return; + } + + info(`Checking loadInfo for URI: ${httpChannel.URI.spec}\n`); + is( + reqLoadInfo.cookieJarSettings.partitionKey, + aPartitionKey, + "The loadInfo has the correct partition key" + ); + + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +add_setup(async function () { + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +add_task(async function testContextMenuSaveImage() { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // We use token to separate the caches. + let token = uuidGenerator.generateUUID().toString(); + const testImageURL = `${TEST_IMAGE_URL}?token=${token}`; + + info(`Open a new tab for testing "Save image as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info(`Insert the testing image into the tab.`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [testImageURL], + async url => { + let img = content.document.createElement("img"); + let loaded = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.setAttribute("id", "image1"); + img.src = url; + await loaded; + } + ); + + info("Open the context menu."); + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#image1", + { + type: "contextmenu", + button: 2, + }, + tab.linkedBrowser + ); + + await popupShownPromise; + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + testImageURL, + partitionKey + ); + + let saveElement = document.getElementById("context-saveimage"); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Check if there will be only one network request. The another one should + // be from cache. + let res = await fetch(`${TEST_IMAGE_URL}?token=${token}&result`); + let res_text = await res.text(); + is(res_text, "1", "The image should be loaded only once."); + + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testContextMenuSaveVideo() { + let uuidGenerator = Services.uuid; + + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + // We use token to separate the caches. + let token = uuidGenerator.generateUUID().toString(); + const testVideoURL = `${TEST_VIDEO_URL}?token=${token}`; + + info(`Open a new tab for testing "Save Video as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info(`Insert the testing video into the tab.`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [testVideoURL], + async url => { + let video = content.document.createElement("video"); + let loaded = new content.Promise(resolve => { + video.onloadeddata = resolve; + }); + content.document.body.appendChild(video); + video.setAttribute("id", "video1"); + video.src = url; + await loaded; + } + ); + + info("Open the context menu."); + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown" + ); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#video1", + { + type: "contextmenu", + button: 2, + }, + tab.linkedBrowser + ); + + await popupShownPromise; + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + // We also check the default file name, see Bug 1679325. + let transferCompletePromise = createPromiseForTransferComplete( + "file_saveAsVideo.webm" + ); + let observerPromise = createPromiseForObservingChannel( + testVideoURL, + partitionKey + ); + + let saveElement = document.getElementById("context-savevideo"); + info("Triggering the save process."); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + info("Close the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.hidePopup(); + await popupHiddenPromise; + + // Check if there will be only one network request. The another one should + // be from cache. + let res = await fetch(`${TEST_VIDEO_URL}?token=${token}&result`); + let res_text = await res.text(); + is(res_text, "1", "The video should be loaded only once."); + + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testSavePageInOfflineMode() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info(`Open a new tab which loads an image`); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_IMAGE_URL + ); + + info("Toggle on the offline mode"); + BrowserOffline.toggleOfflineStatus(); + + info("Open file menu and trigger 'Save Page As'"); + let menubar = document.getElementById("main-menubar"); + let filePopup = document.getElementById("menu_FilePopup"); + + // We only use the shortcut keys to open the file menu in Windows and Linux. + // Mac doesn't have a shortcut to only open the file menu. Instead, we directly + // trigger the save in MAC without any UI interactions. + if (Services.appinfo.OS !== "Darwin") { + let menubarActive = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarActive" + ); + EventUtils.synthesizeKey("KEY_F10"); + await menubarActive; + + let popupShownPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popupshown" + ); + // In window, it still needs one extra down key to open the file menu. + if (Services.appinfo.OS === "WINNT") { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await popupShownPromise; + } + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel( + TEST_IMAGE_URL, + partitionKey + ); + + info("Triggering the save process."); + let fileSavePageAsElement = document.getElementById("menu_savePage"); + fileSavePageAsElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + + // Close the file menu. + if (Services.appinfo.OS !== "Darwin") { + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + filePopup, + "popuphidden" + ); + filePopup.hidePopup(); + await popupHiddenPromise; + } + + info("Toggle off the offline mode"); + BrowserOffline.toggleOfflineStatus(); + + // Clean up + BrowserTestUtils.removeTab(tab); + + // Clean up the cache count on the server side. + await fetch(`${TEST_IMAGE_URL}?result`); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + } + } +}); + +add_task(async function testPageInfoMediaSaveAs() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info( + `Open a new tab for testing "Save AS" in the media panel of the page info.` + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGEINFO_URL + ); + + info("Open the media panel of the pageinfo."); + let pageInfo = BrowserPageInfo( + gBrowser.selectedBrowser.currentURI.spec, + "mediaTab" + ); + + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + let imageTree = pageInfo.document.getElementById("imagetree"); + let imageRowsNum = imageTree.view.rowCount; + + is(imageRowsNum, 2, "There should be two media items here."); + + for (let i = 0; i < imageRowsNum; i++) { + imageTree.view.selection.select(i); + imageTree.ensureRowIsVisible(i); + imageTree.focus(); + + // Wait until the preview is loaded. + let preview = pageInfo.document.getElementById("thepreviewimage"); + let mediaType = pageInfo.gImageView.data[i][1]; // COL_IMAGE_TYPE + if (mediaType == "Image") { + await BrowserTestUtils.waitForEvent(preview, "load"); + } else if (mediaType == "Video") { + await BrowserTestUtils.waitForEvent(preview, "canplaythrough"); + } + + let url = pageInfo.gImageView.data[i][0]; // COL_IMAGE_ADDRESS + info(`Start to save the media item with URL: ${url}`); + + let transferCompletePromise = createPromiseForTransferComplete(); + + // Observe the channel and check if it has the correct partitionKey. + let observerPromise = createPromiseForObservingChannel( + url, + partitionKey + ); + + info("Triggering the save process."); + let saveElement = pageInfo.document.getElementById("imagesaveasbutton"); + saveElement.doCommand(); + + info("Waiting for the channel."); + await observerPromise; + + info("Wait until the save is finished."); + await transferCompletePromise; + } + + pageInfo.close(); + BrowserTestUtils.removeTab(tab); + } + } +}); + +add_task(async function testPageInfoMediaMultipleSelectedSaveAs() { + for (let networkIsolation of [true, false]) { + for (let partitionPerSite of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.partition.network_state", networkIsolation], + ["privacy.dynamic_firstparty.use_site", partitionPerSite], + ], + }); + + let partitionKey = partitionPerSite + ? "(http,example.net)" + : "example.net"; + + info( + `Open a new tab for testing "Save AS" in the media panel of the page info.` + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PAGEINFO_URL + ); + + info("Open the media panel of the pageinfo."); + let pageInfo = BrowserPageInfo( + gBrowser.selectedBrowser.currentURI.spec, + "mediaTab" + ); + + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + + // Make sure the preview image is loaded in order to avoid interfering + // following tests. + let preview = pageInfo.document.getElementById("thepreviewimage"); + await BrowserTestUtils.waitForCondition(() => { + return preview.complete; + }); + + let imageTree = pageInfo.document.getElementById("imagetree"); + let imageRowsNum = imageTree.view.rowCount; + + is(imageRowsNum, 2, "There should be two media items here."); + + imageTree.view.selection.selectAll(); + imageTree.focus(); + + let url = pageInfo.gImageView.data[0][0]; // COL_IMAGE_ADDRESS + info(`Start to save the media item with URL: ${url}`); + + let transferCompletePromise = createPromiseForTransferComplete(); + let observerPromises = []; + + // Observe all channels and check if they have the correct partitionKey. + for (let i = 0; i < imageRowsNum; ++i) { + let observerPromise = createPromiseForObservingChannel( + url, + partitionKey + ); + observerPromises.push(observerPromise); + } + + info("Triggering the save process."); + let saveElement = pageInfo.document.getElementById("imagesaveasbutton"); + saveElement.doCommand(); + + info("Waiting for the all channels."); + await Promise.all(observerPromises); + + info("Wait until the save is finished."); + await transferCompletePromise; + + pageInfo.close(); + BrowserTestUtils.removeTab(tab); + } + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js new file mode 100644 index 0000000000..e131f71169 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_tls_session.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +/** + * Tests that we correctly partition TLS sessions by inspecting the + * socket's peerId. The peerId contains the OriginAttributes suffix which + * includes the partitionKey. + */ + +const TEST_ORIGIN_A = "https://example.com"; +const TEST_ORIGIN_B = "https://example.org"; +const TEST_ORIGIN_C = "https://w3c-test.org"; + +const TEST_ENDPOINT = + "/browser/toolkit/components/antitracking/test/browser/empty.js"; + +const TEST_URL_C = TEST_ORIGIN_C + TEST_ENDPOINT; + +/** + * Waits for a load with the given URL to happen and returns the peerId. + * @param {string} url - The URL expected to load. + * @returns {Promise<string>} a promise which resolves on load with the + * associated socket peerId. + */ +async function waitForLoad(url) { + return new Promise(resolve => { + const TOPIC = "http-on-examine-response"; + + function observer(subject, topic, data) { + if (topic != TOPIC) { + return; + } + subject.QueryInterface(Ci.nsIChannel); + if (subject.URI.spec != url) { + return; + } + + Services.obs.removeObserver(observer, TOPIC); + + resolve(subject.securityInfo.peerId); + } + Services.obs.addObserver(observer, TOPIC); + }); +} + +/** + * Loads a url in the given browser and returns the tls socket's peer id + * associated with the load. + * Note: Loads are identified by URI. If multiple loads with the same URI happen + * concurrently, this method may not map them correctly. + * @param {MozBrowser} browser + * @param {string} url + * @returns {Promise<string>} Resolves on load with the peer id associated with + * the load. + */ +function loadURLInFrame(browser, url) { + let loadPromise = waitForLoad(url); + ContentTask.spawn(browser, [url], async testURL => { + let frame = content.document.createElement("iframe"); + frame.src = testURL; + content.document.body.appendChild(frame); + }); + return loadPromise; +} + +add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ["privacy.partition.network_state", true], + // The test harness acts as a proxy, so we need to make sure to also + // partition for proxies. + ["privacy.partition.network_state.connection_with_proxy", true], + ], + }); + + // C (first party) + let loadPromiseC = waitForLoad(TEST_URL_C); + await BrowserTestUtils.withNewTab(TEST_URL_C, async () => {}); + let peerIdC = await loadPromiseC; + + // C embedded in C (same origin) + let peerIdCC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_C, async browser => { + peerIdCC = await loadURLInFrame(browser, TEST_URL_C); + }); + + // C embedded in A (third party) + let peerIdAC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_A, async browser => { + peerIdAC = await loadURLInFrame(browser, TEST_URL_C); + }); + + // C embedded in B (third party) + let peerIdBC; + await BrowserTestUtils.withNewTab(TEST_ORIGIN_B, async browser => { + peerIdBC = await loadURLInFrame(browser, TEST_URL_C); + }); + + info("Test that top level load and same origin frame have the same peerId."); + is(peerIdC, peerIdCC, "Should have the same peerId"); + + info("Test that all partitioned peer ids are distinct."); + isnot(peerIdCC, peerIdAC, "Should have different peerId partitioned under A"); + isnot(peerIdCC, peerIdBC, "Should have different peerId partitioned under B"); + isnot( + peerIdAC, + peerIdBC, + "Should have a different peerId under different first parties." + ); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js new file mode 100644 index 0000000000..11f53d69ed --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_staticPartition_websocket.js @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const FIRST_PARTY_A = "http://example.com"; +const FIRST_PARTY_B = "http://example.org"; +const THIRD_PARTY = "http://example.net"; +const WS_ENDPOINT_HOST = "mochi.test:8888"; + +function getWSTestUrlForHost(host) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + `ws://${host}` + ) + `file_ws_handshake_delay` + ); +} + +function connect(browsingContext, host, protocol) { + let url = getWSTestUrlForHost(host); + info("Creating websocket with endpoint " + url); + + // Create websocket connection in third party iframe. + let createPromise = SpecialPowers.spawn( + browsingContext.children[0], + [url, protocol], + (url, protocol) => { + let ws = new content.WebSocket(url, [protocol]); + ws.addEventListener("error", () => { + ws._testError = true; + }); + if (!content.ws) { + content.ws = {}; + } + content.ws[protocol] = ws; + } + ); + + let openPromise = createPromise.then(() => + SpecialPowers.spawn( + browsingContext.children[0], + [protocol], + async protocol => { + let ws = content.ws[protocol]; + if (ws.readyState != 0) { + return !ws._testError; + } + // Still connecting. + let result = await Promise.race([ + ContentTaskUtils.waitForEvent(ws, "open"), + ContentTaskUtils.waitForEvent(ws, "error"), + ]); + return result.type != "error"; + } + ) + ); + + let result = { createPromise, openPromise }; + return result; +} + +// Open 3 websockets which target the same ip/port combination, but have +// different principals. We send a protocol identifier to the server to signal +// how long the request should be delayed. +// +// When partitioning is disabled A blocks B and B blocks C. The timeline will +// look like this: +// A________ +// B____ +// C_ +// +// When partitioning is enabled, connection handshakes for A and B will run +// (semi-) parallel since they have different origin attributes. B and C share +// origin attributes and therefore still run serially. +// A________ +// B____ +// C_ +// +// By observing the order of the handshakes we can ensure that the queue +// partitioning is working correctly. +async function runTest(partitioned) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.network_state", partitioned]], + }); + + let tabA = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_A); + await BrowserTestUtils.browserLoaded(tabA.linkedBrowser); + let tabB = BrowserTestUtils.addTab(gBrowser, FIRST_PARTY_B); + await BrowserTestUtils.browserLoaded(tabB.linkedBrowser); + + for (let tab of [tabA, tabB]) { + await SpecialPowers.spawn(tab.linkedBrowser, [THIRD_PARTY], async src => { + let frame = content.document.createElement("iframe"); + frame.src = src; + let loadPromise = ContentTaskUtils.waitForEvent(frame, "load"); + content.document.body.appendChild(frame); + await loadPromise; + }); + } + + // First ensure that we can open websocket connections to the test endpoint. + let { openPromise, createPromise } = await connect( + tabA.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + false + ); + await createPromise; + let openPromiseResult = await openPromise; + ok(openPromiseResult, "Websocket endpoint accepts connections."); + + let openedA; + let openedB; + let openedC; + + let { createPromise: createPromiseA, openPromise: openPromiseA } = connect( + tabA.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-6" + ); + + openPromiseA = openPromiseA.then(opened => { + openedA = opened; + info("Completed WS connection A"); + if (partitioned) { + ok(openedA, "Should have opened A"); + ok(openedB, "Should have opened B"); + } else { + ok(openedA, "Should have opened A"); + ok(openedB == null, "B should be pending"); + } + }); + await createPromiseA; + + // The frame of connection B is embedded in a different first party as A. + let { createPromise: createPromiseB, openPromise: openPromiseB } = connect( + tabB.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-3" + ); + openPromiseB = openPromiseB.then(opened => { + openedB = opened; + info("Completed WS connection B"); + if (partitioned) { + ok(openedA == null, "A should be pending"); + ok(openedB, "Should have opened B"); + ok(openedC == null, "C should be pending"); + } else { + ok(openedA, "Should have opened A"); + ok(openedB, "Should have opened B"); + ok(openedC == null, "C should be pending"); + } + }); + await createPromiseB; + + // The frame of connection C is embedded in the same first party as B. + let { createPromise: createPromiseC, openPromise: openPromiseC } = connect( + tabB.linkedBrowser.browsingContext, + WS_ENDPOINT_HOST, + "test-0" + ); + openPromiseC = openPromiseC.then(opened => { + openedC = opened; + info("Completed WS connection C"); + if (partitioned) { + ok(openedB, "Should have opened B"); + ok(openedC, "Should have opened C"); + } else { + ok(opened, "Should have opened B"); + ok(opened, "Should have opened C"); + } + }); + await createPromiseC; + + // Wait for all connections to complete before closing the tabs. + await Promise.all([openPromiseA, openPromiseB, openPromiseC]); + + BrowserTestUtils.removeTab(tabA); + BrowserTestUtils.removeTab(tabB); + + await SpecialPowers.popPrefEnv(); +} + +add_setup(async function () { + // This test relies on a WS connection timeout > 6 seconds. + await SpecialPowers.pushPrefEnv({ + set: [["network.websocket.timeout.open", 20]], + }); +}); + +add_task(async function test_non_partitioned() { + await runTest(false); +}); + +add_task(async function test_partitioned() { + await runTest(true); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js new file mode 100644 index 0000000000..c2d6898313 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessAutograntRequiresUserInteraction.js @@ -0,0 +1,57 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +async function setAutograntPreferences() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); +} + +add_task(async function testPopupWithUserInteraction() { + await setPreferences(); + await setAutograntPreferences(); + + // Test that requesting storage access initially does not autogrant. + // If the autogrant doesn't occur, we click reject on the door hanger + // and expect the promise returned by requestStorageAccess to reject. + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + // Grant the storageAccessAPI permission to the third-party. + // This signifies that it has been interacted with and should allow autogrants + // among other behaviors. + const uri = Services.io.newURI(TEST_3RD_PARTY_DOMAIN); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + Services.perms.addFromPrincipal( + principal, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + // Test that requesting storage access autogrants here. If a popup occurs, + // expectNoPopup will cause an error in this test. + await openPageAndRunCode( + TEST_TOP_PAGE, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js new file mode 100644 index 0000000000..cf47c43a14 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDeniedGivesNoUserInteraction.js @@ -0,0 +1,30 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testGrantGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await SpecialPowers.testPermission( + "storageAccessAPI", + SpecialPowers.Services.perms.UNKNOWN_ACTION, + { + url: "https://tracking.example.org/", + } + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js new file mode 100644 index 0000000000..733eb34240 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessDoorHanger.js @@ -0,0 +1,372 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); + +const BLOCK = 0; +const ALLOW = 1; + +async function testDoorHanger( + choice, + showPrompt, + useEscape, + topPage, + maxConcurrent, + disableWebcompat = false +) { + info( + `Running doorhanger test with choice #${choice}, showPrompt: ${showPrompt} and ` + + `useEscape: ${useEscape}, topPage: ${topPage}, maxConcurrent: ${maxConcurrent}` + ); + + if (!showPrompt) { + is(choice, ALLOW, "When not showing a prompt, we can only auto-grant"); + ok( + !useEscape, + "When not showing a prompt, we should not be trying to use the Esc key" + ); + } + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.antitracking.enableWebcompat", !disableWebcompat], + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.max_concurrent_auto_grants", maxConcurrent], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + async function runChecks() { + // We need to repeat this constant here since runChecks is stringified + // and sent to the content process. + const BLOCK = 0; + + await new Promise(resolve => { + addEventListener( + "message", + function onMessage(e) { + if (e.data.startsWith("choice:")) { + window.choice = e.data.split(":")[1]; + window.useEscape = e.data.split(":")[3]; + removeEventListener("message", onMessage); + resolve(); + } + }, + false + ); + parent.postMessage("getchoice", "*"); + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + is(document.cookie, "", "Still no cookies for me"); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + if (choice == BLOCK) { + // We've said no, so cookies are still blocked + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookies for me"); + } else { + // We've said yes, so cookies are allowed now + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "I have the cookies!"); + } + } + + let permChanged; + // Only create the promise when we're going to click one of the allow buttons. + if (choice != BLOCK) { + permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => { + let result; + if (choice == ALLOW) { + result = + subject && + subject + .QueryInterface(Ci.nsIPermission) + .type.startsWith("3rdPartyStorage^") && + subject.principal.origin == new URL(topPage).origin && + data == "added"; + } + return result; + }); + } + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise.then(async _ => { + if (topPage != gBrowser.currentURI.spec) { + return; + } + ok(showPrompt, "We shouldn't show the prompt when we don't intend to"); + let notification = await new Promise(function poll(resolve) { + let notification = PopupNotifications.getNotification( + "storage-access", + browser + ); + if (notification) { + resolve(notification); + return; + } + setTimeout(poll, 10); + }); + Assert.ok(notification, "Should have gotten the notification"); + + if (choice == BLOCK) { + if (useEscape) { + EventUtils.synthesizeKey("KEY_Escape", {}, window); + } else { + await clickSecondaryAction(); + } + } else if (choice == ALLOW) { + await clickMainAction(); + } + if (choice != BLOCK) { + await permChanged; + } + }); + + let url = TEST_3RD_PARTY_PAGE + "?disableWaitUntilPermission"; + let ct = SpecialPowers.spawn( + browser, + [{ page: url, callback: runChecks.toString(), choice, useEscape }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + if (event.data == "getchoice") { + ifr.contentWindow.postMessage( + "choice:" + obj.choice + ":useEscape:" + obj.useEscape, + "*" + ); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + if (showPrompt) { + await Promise.all([ct, shownPromise]); + } else { + await Promise.all([ct, permChanged]); + } + + let permissionPopupPromise = BrowserTestUtils.waitForEvent( + window, + "popupshown", + true, + event => event.target == gPermissionPanel._permissionPopup + ); + gPermissionPanel._identityPermissionBox.click(); + await permissionPopupPromise; + let permissionItem = document.querySelector( + ".permission-popup-permission-item-3rdPartyStorage" + ); + ok(permissionItem, "Permission item exists"); + ok( + BrowserTestUtils.is_visible(permissionItem), + "Permission item visible in the identity panel" + ); + let permissionLearnMoreLink = document.getElementById( + "permission-popup-storage-access-permission-learn-more" + ); + ok(permissionLearnMoreLink, "Permission learn more link exists"); + ok( + BrowserTestUtils.is_visible(permissionLearnMoreLink), + "Permission learn more link is visible in the identity panel" + ); + permissionPopupPromise = BrowserTestUtils.waitForEvent( + gPermissionPanel._permissionPopup, + "popuphidden" + ); + gPermissionPanel._permissionPopup.hidePopup(); + await permissionPopupPromise; + + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +async function preparePermissionsFromOtherSites(topPage) { + info("Faking permissions from other sites"); + let type = "3rdPartyStorage^https://tracking.example.org"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + if (topPage == TEST_TOP_PAGE) { + // For the first page, don't do anything + } else if (topPage == TEST_TOP_PAGE_2) { + // For the second page, only add the permission from the first page + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_3) { + // For the third page, add the permissions from the first two pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_4) { + // For the fourth page, add the permissions from the first three pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_5) { + // For the fifth page, add the permissions from the first four pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0); + } else if (topPage == TEST_TOP_PAGE_6) { + // For the sixth page, add the permissions from the first five pages + PermissionTestUtils.add(TEST_DOMAIN, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_2, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_3, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_4, type, permission, expireType, 0); + PermissionTestUtils.add(TEST_DOMAIN_5, type, permission, expireType, 0); + } else { + ok(false, "Unexpected top page: " + topPage); + } +} + +async function cleanUp() { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +async function runRound(topPage, showPrompt, maxConcurrent, disableWebcompat) { + if (showPrompt) { + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + BLOCK, + showPrompt, + true, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + BLOCK, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + ALLOW, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + await cleanUp(); + } else { + await preparePermissionsFromOtherSites(topPage); + await testDoorHanger( + ALLOW, + showPrompt, + false, + topPage, + maxConcurrent, + disableWebcompat + ); + } + await cleanUp(); +} + +add_task(async function test_combinations() { + await runRound(TEST_TOP_PAGE, false, 1); + await runRound(TEST_TOP_PAGE_2, true, 1); + await runRound(TEST_TOP_PAGE, false, 5); + await runRound(TEST_TOP_PAGE_2, false, 5); + await runRound(TEST_TOP_PAGE_3, false, 5); + await runRound(TEST_TOP_PAGE_4, false, 5); + await runRound(TEST_TOP_PAGE_5, false, 5); + await runRound(TEST_TOP_PAGE_6, true, 5); +}); + +add_task(async function test_disableWebcompat() { + await runRound(TEST_TOP_PAGE, true, 5, true); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js new file mode 100644 index 0000000000..13b882c0c7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessFrameInteractionGrantsUserInteraction.js @@ -0,0 +1,70 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); +/* import-globals-from storageAccessAPIHelpers.js */ + +async function testEmbeddedPageBehavior() { + // Get the storage access permission + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + SpecialPowers.wrap(document).clearUserGestureActivation(); + + // Wait until we have the permission before we remove it. + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); + + // Remove the storageAccessAPI permission + SpecialPowers.removePermission( + "storageAccessAPI", + "https://tracking.example.org/" + ); + + // Wait until the permission is removed + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.UNKNOWN_ACTION + ); + + // Interact with the third-party iframe and wait for the permission to appear + SpecialPowers.wrap(document).userInteractionForTesting(); + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); +} + +// This test verifies that interacting with a third-party iframe with +// storage access gives the storageAccessAPI permission, as if it were a first +// party. This is done by loading a page, then within an iframe in that page +// requesting storage access, ensuring there is no storageAccessAPI permission, +// then interacting with the page and waiting for that storageAccessAPI +// permission to reappear. +add_task(async function testInteractionGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + testEmbeddedPageBehavior + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js new file mode 100644 index 0000000000..cd70d8c059 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessGrantedGivesUserInteraction.js @@ -0,0 +1,40 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); +/* import-globals-from storageAccessAPIHelpers.js */ + +async function testEmbeddedPageBehavior() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + SpecialPowers.wrap(document).clearUserGestureActivation(); + waitUntilPermission( + "https://tracking.example.org/", + "storageAccessAPI", + SpecialPowers.Services.perms.ALLOW_ACTION + ); +} + +add_task(async function testGrantGivesPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + testEmbeddedPageBehavior + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js new file mode 100644 index 0000000000..ba109768be --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPrivilegeAPI.js @@ -0,0 +1,617 @@ +// +// Bug 1724376 - Tests for the privilege requestStorageAccessForOrigin API. +// + +/* import-globals-from storageAccessAPIHelpers.js */ + +"use strict"; + +const TEST_ANOTHER_TRACKER_DOMAIN = "https://itisatracker.org/"; +const TEST_ANOTHER_TRACKER_PAGE = + TEST_ANOTHER_TRACKER_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_4TH_PARTY_DOMAIN = "https://test1.example.org/"; +const TEST_ANOTHER_4TH_PARTY_PAGE = + TEST_ANOTHER_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; + +// Insert an iframe with the given id into the content. +async function insertSubFrame(browser, url, id) { + return SpecialPowers.spawn(browser, [url, id], async (url, id) => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", id); + + let loaded = ContentTaskUtils.waitForEvent(ifr, "load", false); + content.document.body.appendChild(ifr); + ifr.src = url; + await loaded; + }); +} + +// Run the given script in the iframe with the given id. +function runScriptInSubFrame(browser, id, script) { + return SpecialPowers.spawn( + browser, + [{ callback: script.toString(), id }], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.getElementById(obj.id); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + ifr.contentWindow.postMessage(obj.callback, "*"); + }); + } + ); +} + +function waitStoragePermission(trackingOrigin) { + return TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + let uri = Services.io.newURI(TEST_DOMAIN); + return ( + permission.type == `3rdPartyStorage^${trackingOrigin}` && + permission.principal.equalsURI(uri) + ); + }); +} + +function clearStoragePermission(trackingOrigin) { + return SpecialPowers.removePermission( + `3rdPartyStorage^${trackingOrigin}`, + TEST_TOP_PAGE + ); +} + +function triggerCommand(button) { + let notifications = PopupNotifications.panel.children; + let notification = notifications[0]; + EventUtils.synthesizeMouseAtCenter(notification[button], {}); +} + +function triggerMainCommand() { + triggerCommand("button"); +} + +function triggerSecondaryCommand() { + triggerCommand("secondaryButton"); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.prompt.testing", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + registerCleanupFunction(() => { + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + UrlClassifierTestUtils.cleanupTestTrackers(); + }); +}); + +add_task(async function test_api_only_available_in_privilege_scope() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.document.requestStorageAccessForOrigin, + "The privilege API is available in system privilege code." + ); + }); + + // Open an iframe and check if the privilege is not available in content code. + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + await runScriptInSubFrame(browser, "test", async function check() { + ok( + !document.requestStorageAccessForOrigin, + "The privilege API is not available in content code." + ); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_privilege_api_with_reject_tracker() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + // Verify that the third party tracker doesn't have storage access at + // beginning. + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "Setting cookie is blocked"); + }); + + let storagePermissionPromise = waitStoragePermission( + "https://tracking.example.org" + ); + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => { + // The privilege API requires user activation. So, we set the user + // activation flag before we call the API. + content.document.notifyUserGestureActivation(); + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org/" + ); + } catch (e) { + ok(false, "The API shouldn't throw."); + } + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + // Accept the prompt + triggerMainCommand(); + + await callAPIPromise; + + // Verify if the storage access permission is set correctly. + await storagePermissionPromise; + + // Verify if the existing third-party tracker iframe gains the storage + // access. + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + + // Insert another third-party tracker iframe and check if it has storage access. + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test2"); + await runScriptInSubFrame(browser, "test2", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "name=value", "Some cookies for me"); + }); + + // Insert another iframe with different third-party tracker and check it has + // no storage access. + await insertSubFrame(browser, TEST_ANOTHER_TRACKER_PAGE, "test3"); + await runScriptInSubFrame(browser, "test3", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "Setting cookie is blocked for another tracker."); + }); + + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_privilege_api_with_dFPI() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await insertSubFrame(browser, TEST_4TH_PARTY_PAGE, "test"); + + // Verify that the third-party context doesn't have storage access at + // beginning. + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=partitioned"; + is( + document.cookie, + "name=partitioned", + "Setting cookie in partitioned context." + ); + }); + + let storagePermissionPromise = waitStoragePermission( + "http://not-tracking.example.com" + ); + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [], async _ => { + // The privilege API requires a user gesture. So, we set the user handling + // flag before we call the API. + content.document.notifyUserGestureActivation(); + + try { + await content.document.requestStorageAccessForOrigin( + "http://not-tracking.example.com/" + ); + } catch (e) { + ok(false, "The API shouldn't throw."); + } + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + // Accept the prompt + triggerMainCommand(); + + await callAPIPromise; + + // Verify if the storage access permission is set correctly. + await storagePermissionPromise; + + // Verify if the existing third-party iframe gains the storage access. + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "No unpartitioned cookies"); + document.cookie = "name=unpartitioned"; + is(document.cookie, "name=unpartitioned", "Successfully set cookies."); + }); + + // Insert another third-party content iframe and check if it has storage access. + await insertSubFrame(browser, TEST_4TH_PARTY_PAGE, "test2"); + await runScriptInSubFrame(browser, "test2", async _ => { + await hasStorageAccessInitially(); + + is( + document.cookie, + "name=unpartitioned", + "Some cookies for unpartitioned context" + ); + }); + + // Insert another iframe with different third-party content and check it has + // no storage access. + await insertSubFrame(browser, TEST_ANOTHER_4TH_PARTY_PAGE, "test3"); + await runScriptInSubFrame(browser, "test3", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "No cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Setting cookie to partitioned context."); + }); + + await clearStoragePermission("http://not-tracking.example.com"); + Services.cookies.removeAll(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_prompt() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + for (const allow of [false, true]) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + // Verify if the prompt has been shown. + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + let hiddenPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn(browser, [allow], async allow => { + // The privilege API requires a user gesture. So, we set the user handling + // flag before we call the API. + content.document.notifyUserGestureActivation(); + let isThrown = false; + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org" + ); + } catch (e) { + isThrown = true; + } + + is(isThrown, !allow, `The API ${allow ? "shouldn't" : "should"} throw.`); + + content.document.clearUserGestureActivation(); + }); + + await shownPromise; + + let notification = await TestUtils.waitForCondition(_ => + PopupNotifications.getNotification("storage-access", browser) + ); + ok(notification, "Should have gotten the notification"); + + // Click the popup button. + if (allow) { + triggerMainCommand(); + } else { + triggerSecondaryCommand(); + } + + // Wait until the popup disappears. + await hiddenPromise; + + // Wait until the API finishes. + await callAPIPromise; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + if (allow) { + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + } else { + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookie after setting."); + }); + } + + BrowserTestUtils.removeTab(tab); + } + + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); +}); + +// Tests that the priviledged rSA method should show a prompt when auto grants +// are enabled, but we don't have user activation. When requiring user +// activation, rSA should still reject. +add_task(async function test_prompt_no_user_activation() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ], + }); + + for (let requireUserActivation of [false, true]) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + let shownPromise, hiddenPromise; + + // Verify if the prompt has been shown. + if (!requireUserActivation) { + shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + hiddenPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + } + + // Call the privilege API. + let callAPIPromise = SpecialPowers.spawn( + browser, + [requireUserActivation], + async requireUserActivation => { + let isThrown = false; + + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org", + requireUserActivation + ); + } catch (e) { + isThrown = true; + } + + is( + isThrown, + requireUserActivation, + `The API ${requireUserActivation ? "shouldn't" : "should"} throw.` + ); + } + ); + + if (!requireUserActivation) { + await shownPromise; + + let notification = await TestUtils.waitForCondition(_ => + PopupNotifications.getNotification("storage-access", browser) + ); + ok(notification, "Should have gotten the notification"); + + // Click the popup button. + triggerMainCommand(); + + // Wait until the popup disappears. + await hiddenPromise; + } + + // Wait until the API finishes. + await callAPIPromise; + + await insertSubFrame(browser, TEST_3RD_PARTY_PAGE, "test"); + + if (!requireUserActivation) { + await runScriptInSubFrame(browser, "test", async _ => { + await hasStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "name=value", "Successfully set cookies."); + }); + } else { + await runScriptInSubFrame(browser, "test", async _ => { + await noStorageAccessInitially(); + + is(document.cookie, "", "Still no cookies for me"); + document.cookie = "name=value"; + is(document.cookie, "", "No cookie after setting."); + }); + } + + BrowserTestUtils.removeTab(tab); + await clearStoragePermission("https://tracking.example.org"); + Services.cookies.removeAll(); + } +}); + +add_task(async function test_invalid_input() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + let browser = tab.linkedBrowser; + + await SpecialPowers.spawn(browser, [], async _ => { + let isThrown = false; + try { + await content.document.requestStorageAccessForOrigin( + "https://tracking.example.org" + ); + } catch (e) { + isThrown = true; + } + ok(isThrown, "The API should throw without user gesture."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin(); + } catch (e) { + isThrown = true; + } + ok(isThrown, "The API should throw with no input."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin(""); + } catch (e) { + isThrown = true; + is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url"); + } + ok(isThrown, "The API should throw with empty string."); + + content.document.notifyUserGestureActivation(); + isThrown = false; + try { + await content.document.requestStorageAccessForOrigin("invalid url"); + } catch (e) { + isThrown = true; + is(e.name, "NS_ERROR_MALFORMED_URI", "The input is not a valid url"); + } + ok(isThrown, "The API should throw with invalid url."); + + content.document.clearUserGestureActivation(); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js new file mode 100644 index 0000000000..835c02e262 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction.js @@ -0,0 +1,36 @@ +AntiTracking.runTest( + "Storage Access API returns promises that do not maintain user activation for calling its reject handler", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise reject handler must not have user activation" + ); + }, true); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess should not be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // expected blocking notifications + false, // private window + "allow-scripts allow-same-origin allow-popups" // iframe sandbox +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js new file mode 100644 index 0000000000..21bf8b7639 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseRejectHandlerUserInteraction_alwaysPartition.js @@ -0,0 +1,31 @@ +AntiTracking.runTest( + "Storage Access API returns promises that do not maintain user activation for calling its reject handler", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + !SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise reject handler must not have user activation" + ); + }, true); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess should not be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // private window + "allow-scripts allow-same-origin allow-popups" // iframe sandbox +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js new file mode 100644 index 0000000000..6db7c4c241 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessPromiseResolveHandlerUserInteraction.js @@ -0,0 +1,37 @@ +AntiTracking.runTest( + "Storage Access API returns promises that maintain user activation", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(() => { + ok( + SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise handler must run as if we're handling user input" + ); + }); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + + await document.hasStorageAccess(); + + ok( + SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, + "Promise handler must run as if we're handling user input" + ); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + null, // extra prefs + false, // no window open test + false // no user-interaction test +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js new file mode 100644 index 0000000000..f658dde790 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe.js @@ -0,0 +1,46 @@ +AntiTracking.runTest( + "Storage Access is removed when subframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-subframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + // TODO: this is just a temporarily fixed, we should update the testcase + // in Bug 1649399 + await hasStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js new file mode 100644 index 0000000000..b4355f2a24 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateSubframe_alwaysPartition.js @@ -0,0 +1,41 @@ +AntiTracking.runTest( + "Storage Access is removed when subframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-subframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + // TODO: this is just a temporarily fixed, we should update the testcase + // in Bug 1649399 + await hasStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js new file mode 100644 index 0000000000..fe79a8d935 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe.js @@ -0,0 +1,44 @@ +AntiTracking.runTest( + "Storage Access is removed when topframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-topframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js new file mode 100644 index 0000000000..63a0480e83 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessRemovalNavigateTopframe_alwaysPartition.js @@ -0,0 +1,39 @@ +AntiTracking.runTest( + "Storage Access is removed when topframe navigates", + // blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }, + + // non-blocking callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + + /* import-globals-from storageAccessAPIHelpers.js */ + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [["privacy.partition.always_partition_third_party_non_cookie_storage", true]], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + null, // no iframe sandbox + "navigate-topframe", // access removal type + // after-removal callback + async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + } +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js new file mode 100644 index 0000000000..d83bcf4b7b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed.js @@ -0,0 +1,214 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Exception List can work in a sandboxed iframe", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + try { + await navigator.serviceWorker.register("empty.js"); + + ok( + true, + "ServiceWorker can be registered in allowlisted sandboxed iframe!" + ); + } catch (e) { + info("Promise rejected: " + e); + ok( + false, + "ServiceWorker should be able to be registered in allowlisted sandboxed iframe" + ); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [ + "privacy.restrict3rdpartystorage.skip_list", + "http://example.net,https://tracking.example.org", + ], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe with" + + " allow-storage-access-by-user-activation", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts with" + + " allow-storage-access-by-user-activation get the" + + " saved permission", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that private browsing contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + true, // run in private window + null // iframe sandbox +); + +AntiTracking.runTest( + "Verify that non-sandboxed contexts get the saved permission", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, false], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0 // no blocking notifications +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js new file mode 100644 index 0000000000..402ba74ec8 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessSandboxed_alwaysPartition.js @@ -0,0 +1,214 @@ +/* import-globals-from storageAccessAPIHelpers.js */ + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe", + // blocking callback + async _ => { + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(rejected, "requestStorageAccess shouldn't be available"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + // Only clear the user-interaction permissions for the tracker here so that + // the next test has a clean slate. + await new Promise(resolve => { + Services.clearData.deleteDataFromHost( + Services.io.newURI(TEST_3RD_PARTY_DOMAIN).host, + true, + Ci.nsIClearDataService.CLEAR_PERMISSIONS, + value => resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expected blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Exception List can work in a sandboxed iframe", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + try { + await navigator.serviceWorker.register("empty.js"); + + ok( + true, + "ServiceWorker can be registered in allowlisted sandboxed iframe!" + ); + } catch (e) { + info("Promise rejected: " + e); + ok( + false, + "ServiceWorker should be able to be registered in allowlisted sandboxed iframe" + ); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [ + "privacy.restrict3rdpartystorage.skip_list", + "http://example.net,https://tracking.example.org", + ], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Storage Access API called in a sandboxed iframe with" + + " allow-storage-access-by-user-activation", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + let [threw, rejected] = await callRequestStorageAccess(); + ok(!threw, "requestStorageAccess should not throw"); + ok(!rejected, "requestStorageAccess should be available"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups" +); + +AntiTracking.runTest( + "Verify that sandboxed contexts with" + + " allow-storage-access-by-user-activation get the" + + " saved permission", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0, // no blocking notifications + false, // run in normal window + "allow-scripts allow-same-origin allow-popups allow-storage-access-by-user-activation" +); + +AntiTracking.runTest( + "Verify that private browsing contexts don't get the saved permission", + // blocking callback + async _ => { + await noStorageAccessInitially(); + + try { + localStorage.foo = 42; + ok(false, "LocalStorage cannot be used!"); + } catch (e) { + ok(true, "LocalStorage cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, + + null, // non-blocking callback + null, // cleanup function + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, // expect blocking notifications + true, // run in private window + null // iframe sandbox +); + +AntiTracking.runTest( + "Verify that non-sandboxed contexts get the saved permission", + // blocking callback + async _ => { + await hasStorageAccessInitially(); + + localStorage.foo = 42; + ok(true, "LocalStorage can be used!"); + }, + + null, // non-blocking callback + // cleanup function + async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + }, + [ + ["dom.storage_access.enabled", true], + [APS_PREF, true], + ], // extra prefs + false, // no window open test + false, // no user-interaction test + 0 // no blocking notifications +); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js new file mode 100644 index 0000000000..5b3edec49e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeDifferentSite.js @@ -0,0 +1,64 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testDifferentSitePermission() { + await setPreferences(/*alwaysPartitionStorage*/ false); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testDifferentSitePermissionAPS() { + await setPreferences(/*alwaysPartitionStorage*/ true); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js new file mode 100644 index 0000000000..f0b1aca0b6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameOrigin.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameOriginPermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js new file mode 100644 index 0000000000..0dd893eda6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteRead.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameSitePermission() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_8, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js new file mode 100644 index 0000000000..4b99cb266e --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessScopeSameSiteWrite.js @@ -0,0 +1,43 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +add_task(async function testInitialBlock() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + getExpectPopupAndClick("reject"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectFailure + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testSameSitePermissionReversed() { + await setPreferences(); + + await openPageAndRunCode( + TEST_TOP_PAGE_8, + getExpectPopupAndClick("accept"), + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await openPageAndRunCode( + TEST_TOP_PAGE_7, + expectNoPopup, + TEST_3RD_PARTY_PAGE, + requestStorageAccessAndExpectSuccess + ); + + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js new file mode 100644 index 0000000000..5a8c7970c5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks.js @@ -0,0 +1,157 @@ +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking._createTask({ + name: "Test that after a storage access grant we have full first-party access", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(); + + const TRACKING_PAGE = + "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/trackingPage.html"; + async function runChecks(name) { + let iframe = document.createElement("iframe"); + iframe.src = TRACKING_PAGE; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [name], name => { + content.postMessage(name, "*"); + }); + + await new Promise(resolve => { + onmessage = e => { + if (e.data == "done") { + resolve(); + } + }; + }); + } + + await runChecks("image"); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + "http://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=2", + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=3", + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, false]], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js new file mode 100644 index 0000000000..1ba0b89230 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessThirdPartyChecks_alwaysPartition.js @@ -0,0 +1,153 @@ +const allBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL; +const foreignBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN; + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +AntiTracking._createTask({ + name: "Test that after a storage access grant we have full first-party access", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(); + + const TRACKING_PAGE = + "http://another-tracking.example.net/browser/browser/base/content/test/protectionsUI/trackingPage.html"; + async function runChecks(name) { + let iframe = document.createElement("iframe"); + iframe.src = TRACKING_PAGE; + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.onload = resolve; + }); + + await SpecialPowers.spawn(iframe, [name], name => { + content.postMessage(name, "*"); + }); + + await new Promise(resolve => { + onmessage = e => { + if (e.data == "done") { + resolve(); + } + }; + }); + } + + await runChecks("image"); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: [ + "http://tracking.example.org", + "http://tracking.example.org", + "http://tracking.example.org", + "http://tracking.example.org", + "http://trackertest.org", + ], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=1, when network.cookie.rejectForeignWithExceptions.enabled is set to false", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [ + ["network.cookie.rejectForeignWithExceptions.enabled", false], + [APS_PREF, true], + ], + expectedBlockingNotifications: foreignBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: ["http://tracking.example.org"], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=2", + cookieBehavior: BEHAVIOR_REJECT, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: allBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: ["http://example.net", "http://tracking.example.org"], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we never grant access to cookieBehavior=3", + cookieBehavior: BEHAVIOR_LIMIT_FOREIGN, + allowList: false, + callback: async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + + await callRequestStorageAccess(null, true); + }, + extraPrefs: [[APS_PREF, true]], + expectedBlockingNotifications: foreignBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + thirdPartyPage: TEST_3RD_PARTY_PAGE_HTTP, + errorMessageDomains: ["http://tracking.example.org"], +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js new file mode 100644 index 0000000000..f50e515c84 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithDynamicFpi.js @@ -0,0 +1,612 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "peuService", + "@mozilla.org/partitioning/exception-list-service;1", + "nsIPartitioningExceptionListService" +); + +const TEST_REDIRECT_TOP_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_TOP_PAGE; +const TEST_REDIRECT_3RD_PARTY_PAGE = + TEST_DOMAIN + TEST_PATH + "redirect.sjs?" + TEST_3RD_PARTY_PARTITIONED_PAGE; + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const EXCEPTION_LIST_PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +async function cleanup() { + Services.prefs.clearUserPref(EXCEPTION_LIST_PREF_NAME); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_setup(async function () { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["privacy.restrict3rdpartystorage.heuristic.redirect", false], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + registerCleanupFunction(cleanup); +}); + +function executeContentScript(browser, callback, options = {}) { + return SpecialPowers.spawn( + browser, + [ + { + callback: callback.toString(), + ...options, + }, + ], + obj => { + return new content.Promise(async resolve => { + if (obj.page) { + // third-party + let ifr = content.document.createElement("iframe"); + ifr.onload = async () => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage( + { cb: obj.callback, value: obj.value }, + "*" + ); + }; + + content.addEventListener("message", event => resolve(event.data), { + once: true, + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + } else { + // first-party + let runnableStr = `(() => {return (${obj.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + resolve(await runnable.call(content, content, obj.value)); + } + }); + } + ); +} + +function readNetworkCookie(win) { + return win + .fetch("cookies.sjs") + .then(r => r.text()) + .then(text => { + return text.substring("cookie:foopy=".length); + }); +} + +async function writeNetworkCookie(win, value) { + await win.fetch("cookies.sjs?" + value).then(r => r.text()); + return true; +} + +function createDataInFirstParty(browser, value) { + return executeContentScript(browser, writeNetworkCookie, { value }); +} +function getDataFromFirstParty(browser) { + return executeContentScript(browser, readNetworkCookie, {}); +} +function createDataInThirdParty(browser, value) { + return executeContentScript(browser, writeNetworkCookie, { + page: TEST_3RD_PARTY_PARTITIONED_PAGE, + value, + }); +} +function getDataFromThirdParty(browser) { + return executeContentScript(browser, readNetworkCookie, { + page: TEST_3RD_PARTY_PARTITIONED_PAGE, + }); +} + +async function redirectWithUserInteraction(browser, url, wait = null) { + await executeContentScript( + browser, + (content, value) => { + content.document.userInteractionForTesting(); + + let link = content.document.createElement("a"); + link.appendChild(content.document.createTextNode("click me!")); + link.href = value; + content.document.body.appendChild(link); + link.click(); + }, + { + value: url, + } + ); + await BrowserTestUtils.browserLoaded(browser, false, wait || url); +} + +async function checkData(browser, options) { + if ("firstParty" in options) { + is( + await getDataFromFirstParty(browser), + options.firstParty, + "correct first-party data" + ); + } + if ("thirdParty" in options) { + is( + await getDataFromThirdParty(browser), + options.thirdParty, + "correct third-party data" + ); + } +} + +async function runTestRedirectHeuristic(disableHeuristics) { + info("Starting Dynamic FPI Redirect Heuristic test..."); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", true], + ["privacy.antitracking.enableWebcompat", !disableHeuristics], + ], + }); + + // mark third-party as tracker + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("initializing..."); + await checkData(browser, { firstParty: "", thirdParty: "" }); + + await Promise.all([ + createDataInFirstParty(browser, "firstParty"), + createDataInThirdParty(browser, "thirdParty"), + ]); + + await checkData(browser, { + firstParty: "firstParty", + thirdParty: "", + }); + + info("load third-party content as first-party"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_3RD_PARTY_PAGE, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + + await checkData(browser, { firstParty: "" }); + await createDataInFirstParty(browser, "heuristicFirstParty"); + await checkData(browser, { firstParty: "heuristicFirstParty" }); + + info("redirect back to first-party page"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_TOP_PAGE, + TEST_TOP_PAGE + ); + + info("third-party tracker should NOT able to access first-party data"); + await checkData(browser, { + firstParty: "firstParty", + thirdParty: "", + }); + + // remove third-party from tracker + await UrlClassifierTestUtils.cleanupTestTrackers(); + + info("load third-party content as first-party"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_3RD_PARTY_PAGE, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + + await checkData(browser, { + firstParty: "heuristicFirstParty", + }); + + info("redirect back to first-party page"); + await redirectWithUserInteraction( + browser, + TEST_REDIRECT_TOP_PAGE, + TEST_TOP_PAGE + ); + + info( + `third-party page should ${ + disableHeuristics ? "not " : "" + }be able to access first-party data` + ); + await checkData(browser, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "" : "heuristicFirstParty", + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + await cleanup(); +} + +add_task(async function testRedirectHeuristic() { + await runTestRedirectHeuristic(false); +}); + +add_task(async function testRedirectHeuristicDisabled() { + await runTestRedirectHeuristic(true); +}); + +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return new Promise(function (resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +// The test URLs have a trailing / which means they're not valid origins. +const TEST_ORIGIN = TEST_DOMAIN.substring(0, TEST_DOMAIN.length - 1); +const TEST_3RD_PARTY_ORIGIN = TEST_3RD_PARTY_DOMAIN.substring( + 0, + TEST_3RD_PARTY_DOMAIN.length - 1 +); + +async function runTestExceptionListPref(disableHeuristics) { + info("Starting Dynamic FPI exception list test pref"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ["privacy.antitracking.enableWebcompat", !disableHeuristics], + ], + }); + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set incomplete exception list pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN}`); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref, with extra semicolons"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `;${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN};;` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list pref, with subdomain wildcard"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN.replace("tracking", "*")}` + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: disableHeuristics ? "thirdParty" : "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + await SpecialPowers.popPrefEnv(); + + await cleanup(); +} + +add_task(async function testExceptionListPref() { + await runTestExceptionListPref(false); +}); + +add_task(async function testExceptionListPrefDisabled() { + await runTestExceptionListPref(true); +}); + +add_task(async function testExceptionListRemoteSettings() { + info("Starting Dynamic FPI exception list test (remote settings)"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ], + }); + + // Make sure we have a pref initially, since the exception list service + // requires it. + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, ""); + + // Add some initial data + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // make peuSerivce start working by calling + // registerAndRunExceptionListObserver + let updateEvent = new UpdateEvent(); + let obs = data => { + let event = new CustomEvent("update", { detail: data }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(updateEvent, "update"); + peuService.registerAndRunExceptionListObserver(obs); + await promise; + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set exception list remote settings"); + + // set records + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + firstPartyOrigin: TEST_ORIGIN, + thirdPartyOrigin: TEST_3RD_PARTY_ORIGIN, + }, + ], + }, + }); + + let list = await promise; + is( + list, + `${TEST_ORIGIN},${TEST_3RD_PARTY_ORIGIN}`, + "exception list is correctly set" + ); + + info("check data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [], + }, + }); + is(await promise, "", "Exception list is cleared"); + + peuService.unregisterExceptionListObserver(obs); + await cleanup(); +}); + +add_task(async function testWildcardExceptionListPref() { + info("Starting Dynamic FPI wirdcard exception list test pref"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.restrict3rdpartystorage.heuristic.recently_visited", false], + ], + }); + + info("Creating new tabs"); + let tabThirdParty = BrowserTestUtils.addTab( + gBrowser, + TEST_3RD_PARTY_PARTITIONED_PAGE + ); + gBrowser.selectedTab = tabThirdParty; + + let browserThirdParty = gBrowser.getBrowserForTab(tabThirdParty); + await BrowserTestUtils.browserLoaded(browserThirdParty); + + let tabFirstParty = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tabFirstParty; + + let browserFirstParty = gBrowser.getBrowserForTab(tabFirstParty); + await BrowserTestUtils.browserLoaded(browserFirstParty); + + info("initializing..."); + await Promise.all([ + checkData(browserFirstParty, { firstParty: "", thirdParty: "" }), + checkData(browserThirdParty, { firstParty: "" }), + ]); + + info("fill default data"); + await Promise.all([ + createDataInFirstParty(browserFirstParty, "firstParty"), + createDataInThirdParty(browserFirstParty, "thirdParty"), + createDataInFirstParty(browserThirdParty, "ExceptionListFirstParty"), + ]); + + info("check initial data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set wildcard (1st-party) pref"); + Services.prefs.setStringPref( + EXCEPTION_LIST_PREF_NAME, + `*,${TEST_3RD_PARTY_ORIGIN}` + ); + + info("check wildcard (1st-party) data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set invalid exception list pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, "*,*"); + + info("check initial data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "thirdParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("set wildcard (3rd-party) pref"); + Services.prefs.setStringPref(EXCEPTION_LIST_PREF_NAME, `${TEST_ORIGIN},*`); + + info("check wildcard (3rd-party) data"); + await Promise.all([ + checkData(browserFirstParty, { + firstParty: "firstParty", + thirdParty: "ExceptionListFirstParty", + }), + checkData(browserThirdParty, { firstParty: "ExceptionListFirstParty" }), + ]); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tabFirstParty); + BrowserTestUtils.removeTab(tabThirdParty); + + await cleanup(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js new file mode 100644 index 0000000000..5b68975d03 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccessWithHeuristics.js @@ -0,0 +1,912 @@ +function waitStoragePermission() { + return new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission( + `3rdPartyStorage^${TEST_3RD_PARTY_DOMAIN.slice(0, -1)}`, + SpecialPowers.Services.perms.ALLOW_ACTION, + TEST_DOMAIN + ) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); +} + +add_setup(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); +}); + +async function runTestWindowOpenHeuristic(disableHeuristics) { + info( + `Starting window.open() heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + disableHeuristics, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testWindowOpenHeuristic() { + await runTestWindowOpenHeuristic(false); +}); + +add_task(async function testWindowOpenHeuristicDisabled() { + await runTestWindowOpenHeuristic(true); +}); + +add_task(async function testDoublyNestedWindowOpenHeuristic() { + info("Starting doubly nested window.open() heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_WO, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +async function runTestUserInteractionHeuristic(disableHeuristics) { + info( + `Starting user interaction heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI for Fission. It's because the + // 'domwindowclosed' would be triggered twice, one for the + // 'about:blank' page and another for the tracker page. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + disableHeuristics, + }, + ], + async obj => { + let msg = {}; + + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + // We need to check the document URI here as well for the same + // reason above. + if ( + aTopic == "domwindowclosed" && + aSubject.document.documentURI == + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html" + ) { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + content.open(popup); + + info("Let's wait for the window to be closed"); + await windowClosed; + }); + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: msg.nonBlockingCallback }, + "*" + ); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (!disableHeuristics) { + info("Wait until the storage permission is ready before cleaning up."); + await waitStoragePermission(); + } + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + + await SpecialPowers.popPrefEnv(); +} + +add_task(async function testUserInteractionHeuristic() { + await runTestUserInteractionHeuristic(false); +}); + +add_task(async function testUserInteractionHeuristicDisabled() { + await runTestUserInteractionHeuristic(true); +}); + +add_task(async function testDoublyNestedUserInteractionHeuristic() { + info("Starting doubly nested user interaction heuristic test..."); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + }).toString(); + + msg.openWindowCallback = (async url => { + open(url); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + ifr.contentWindow.postMessage( + { callback: msg.openWindowCallback, arg: obj.popup }, + "*" + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + } + ); + + await AntiTracking.interactWithTracker(); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_RELAY + "?" + TEST_3RD_PARTY_PAGE_UI, + popup: TEST_POPUP_PAGE, + }, + ], + async obj => { + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + + msg.openWindowCallback = (async url => { + open(url); + }).toString(); + + info("Checking if storage access is denied"); + + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info( + "The 3rd party content should not have access to first party storage." + ); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage({ callback: msg.blockingCallback }, "*"); + }); + + let windowClosed = new content.Promise(resolve => { + Services.ww.registerNotification(function notification( + aSubject, + aTopic, + aData + ) { + if (aTopic == "domwindowclosed") { + Services.ww.unregisterNotification(notification); + resolve(); + } + }); + }); + + info("Opening a window from the iframe."); + ifr.contentWindow.postMessage( + { callback: msg.openWindowCallback, arg: obj.popup }, + "*" + ); + + info("Let's wait for the window to be closed"); + await windowClosed; + + info("The 3rd party content should have access to first party storage."); + await new content.Promise(resolve => { + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + ifr.contentWindow.postMessage( + { callback: msg.nonBlockingCallback }, + "*" + ); + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function () { + info("Wait until the storage permission is ready before cleaning up."); + await waitStoragePermission(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +async function runTestFirstPartyWindowOpenHeuristic(disableHeuristics) { + info( + `Starting first-party window.open() heuristic test with heuristic ${ + disableHeuristics ? "disabled" : "enabled" + }.` + ); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.antitracking.enableWebcompat", !disableHeuristics]], + }); + + // Interact with the tracker first before testing window.open heuristic + await AntiTracking.interactWithTracker(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE, + }, + ], + async obj => { + info("Tracker shouldn't have storage access initially"); + let msg = {}; + msg.blockingCallback = (async _ => { + await noStorageAccessInitially(); + }).toString(); + + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg.blockingCallback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.id = "ifr"; + ifr.src = obj.page; + }); + } + ); + + info("Calling window.open in a first-party iframe"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_IFRAME_PAGE, + popup: TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyOpen.html", + }, + ], + async obj => { + let ifr = content.document.createElement("iframe"); + let loading = new content.Promise(resolve => { + ifr.onload = resolve; + }); + content.document.body.appendChild(ifr); + ifr.src = obj.page; + await loading; + + info("Opening a window from the iframe."); + await SpecialPowers.spawn(ifr, [obj.popup], async popup => { + await new content.Promise(resolve => { + content.open(popup); + content.addEventListener("message", function msg(event) { + if (event.data == "hello!") { + resolve(); + } + }); + }); + }); + } + ); + + await SpecialPowers.spawn(browser, [{ disableHeuristics }], async obj => { + info( + "If the heuristic is enabled, the tracker should have storage access now." + ); + let msg = {}; + + // If the heuristic is disabled, we won't get storage access. + if (obj.disableHeuristics) { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await stillNoStorageAccess(); + }).toString(); + } else { + msg.nonBlockingCallback = (async _ => { + /* import-globals-from storageAccessAPIHelpers.js */ + await hasStorageAccessInitially(); + }).toString(); + } + + await new content.Promise(resolve => { + let ifr = content.document.getElementById("ifr"); + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(msg.nonBlockingCallback, "*"); + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); + + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testFirstPartyWindowOpenHeuristic() { + await runTestFirstPartyWindowOpenHeuristic(false); +}); + +add_task(async function testFirstPartyWindowOpenHeuristicDisabled() { + await runTestFirstPartyWindowOpenHeuristic(true); +}); + +add_task(async function () { + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js new file mode 100644 index 0000000000..a88fc8bcb3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Arguments.js @@ -0,0 +1,117 @@ +add_task(async function testArgumentInRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite("blob://test"); + try { + await p; + ok(false, "Blob URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite("about:config"); + try { + await p; + ok(false, "about URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite("qwertyuiop"); + try { + await p; + ok(false, "Non URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.requestStorageAccessUnderSite(""); + try { + await p; + ok(false, "Nullstring must be rejected."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testArgumentInCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = + content.document.completeStorageAccessRequestFromSite("blob://test"); + try { + await p; + ok(false, "Blob URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite("about:config"); + try { + await p; + ok(false, "about URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite("qwertyuiop"); + try { + await p; + ok(false, "Non URLs must be rejected."); + } catch { + ok(true, "Must reject."); + } + + p = content.document.completeStorageAccessRequestFromSite(""); + try { + await p; + ok(false, "Nullstring must be rejected."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js new file mode 100644 index 0000000000..ea44a34f24 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookieBehavior.js @@ -0,0 +1,412 @@ +add_task(async function testBehaviorAcceptRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorAcceptCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorRejectRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testBehaviorRejectCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function testBehaviorLimitForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async function testBehaviorLimitForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function testBehaviorRejectForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_DOMAIN, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerAndPartitionForeignRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_DOMAIN, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testBehaviorRejectTrackerAndPartitionForeignCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js new file mode 100644 index 0000000000..64ead20020 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CookiePermission.js @@ -0,0 +1,174 @@ +add_task(async _ => { + PermissionTestUtils.add( + TEST_4TH_PARTY_PAGE, + "cookie", + Services.perms.ALLOW_ACTION + ); +}); + +add_task(async function testCookiePermissionRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); +}); + +add_task(async function testCookiePermissionCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: Services.perms.ALLOW_ACTION, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); +}); + +add_task(async _ => { + Services.perms.removeAll(); +}); + +add_task(async _ => { + PermissionTestUtils.add( + TEST_4TH_PARTY_PAGE, + "cookie", + Services.perms.DENY_ACTION + ); +}); + +add_task( + async function testCookiePermissionRejectRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); + } +); + +add_task( + async function testCookiePermissionRejectCompleteStorageAccessRequest() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: Services.perms.ALLOW_ACTION, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPermissions(); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js new file mode 100644 index 0000000000..ca3e47d8e7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_CrossOriginSameSite.js @@ -0,0 +1,162 @@ +add_task(async function testIntermediatePreferenceReadSameSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_DOMAIN_7, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because we don't have the initial request."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.com", + allow: 1, + context: TEST_DOMAIN_7, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because the permission is cross site."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_DOMAIN_7, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_DOMAIN_8, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); +}); + +// Note: TEST_DOMAIN_7 and TEST_DOMAIN_8 are Same-Site +add_task(async function testIntermediatePreferenceWriteCrossOrigin() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN_8], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok( + true, + "Must resolve- no funny business here, we just want to set the intermediate pref" + ); + } catch { + ok(false, "Must not reject."); + } + }); + + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_DOMAIN_8 + ); + // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE + var permission = Services.perms.testPermissionFromPrincipal( + principal, + "AllowStorageAccessRequest^https://example.org" + ); + ok(permission == Services.perms.ALLOW_ACTION); + + // Test that checking the permission across site works + principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TEST_DOMAIN_7 + ); + // Important to note that this is the site but not origin of TEST_3RD_PARTY_PAGE + permission = Services.perms.testPermissionFromPrincipal( + principal, + "AllowStorageAccessRequest^https://example.org" + ); + ok(permission == Services.perms.ALLOW_ACTION); + + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js new file mode 100644 index 0000000000..e8d6ce9bb1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Doorhanger.js @@ -0,0 +1,122 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/modules/test/browser/head.js", + this +); + +async function cleanUp() { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +} + +add_task(async function testDoorhangerRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + let permChanged = TestUtils.topicObserved("perm-changed", (subject, data) => { + let result = + subject + .QueryInterface(Ci.nsIPermission) + .type.startsWith("AllowStorageAccessRequest^") && + subject.principal.origin == new URL(TEST_TOP_PAGE).origin && + data == "added"; + return result; + }).then(() => { + ok(true, "Permission changed to add intermediate permission"); + }); + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ).then(_ => { + ok(true, "Must display doorhanger from RequestStorageAccessUnderSite"); + return clickMainAction(); + }); + let sp = SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + let p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await Promise.all([sp, shownPromise, permChanged]); + await cleanUp(); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testNoDoorhangerCompleteStorageAccessRequestFromSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.prompt.testing", false], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + let popupShown = false; + // This promise is used to the absence of a doorhanger showing up. + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown") + .then(_ => { + // This will be called if a doorhanger is shown. + ok( + false, + "Must not display doorhanger from CompleteStorageAccessRequestFromSite" + ); + popupShown = true; + }) + .catch(_ => { + // This will be called when the test ends if a doorhanger is not shown + ok(true, "It is expected for this popup to not show up."); + }); + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + let p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + ok(!popupShown, "Must not have shown a popup during this test."); + await cleanUp(); + await BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js new file mode 100644 index 0000000000..86f584763c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Embed.js @@ -0,0 +1,155 @@ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/storage_access_head.js", + this +); + +async function requestStorageAccessUnderSiteAndExpectSuccess() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccessUnderSite("http://example.org"); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } +} + +async function requestStorageAccessUnderSiteAndExpectFailure() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.requestStorageAccessUnderSite("http://example.org"); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } +} + +async function completeStorageAccessRequestFromSiteAndExpectSuccess() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.completeStorageAccessRequestFromSite("http://example.org"); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } +} + +async function completeStorageAccessRequestFromSiteAndExpectFailure() { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + var p = document.completeStorageAccessRequestFromSite("http://example.org"); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject."); + } +} + +async function setIntermediatePreference() { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.org", + allow: 1, + context: "http://example.com/", + }, + ]); +} + +async function configurePrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); +} + +add_task(async function rSAUS_sameOriginIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function rSAUS_sameSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function rSAUS_crossSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN + TEST_PATH + "3rdParty.html", + requestStorageAccessUnderSiteAndExpectFailure + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_sameOriginIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + setIntermediatePreference, + TEST_DOMAIN_7 + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_sameSiteIframe() { + await configurePrefs(); + await setIntermediatePreference(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + () => {}, + TEST_DOMAIN_8 + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectSuccess + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async function cSAR_crossSiteIframe() { + await configurePrefs(); + await openPageAndRunCode( + TEST_TOP_PAGE_7, + setIntermediatePreference, + TEST_DOMAIN + TEST_PATH + "3rdParty.html", + completeStorageAccessRequestFromSiteAndExpectFailure + ); + await cleanUpData(); + await SpecialPowers.flushPrefEnv(); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js new file mode 100644 index 0000000000..6ee61cc378 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_Enable.js @@ -0,0 +1,86 @@ +add_task(async function testDefaultDisabled() { + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(!value, "dom.storage_access.forward_declared.enabled should be false"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.window.requestStorageAccessUnderSite == undefined, + "API should not be on the window" + ); + ok( + content.window.completeStorageAccessRequestFromSite == undefined, + "API should not be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testExplicitlyDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.storage_access.forward_declared.enabled", false]], + }); + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(!value, "dom.storage_access.forward_declared.enabled should be false"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.window.requestStorageAccessUnderSite == undefined, + "API should not be on the window" + ); + ok( + content.window.completeStorageAccessRequestFromSite == undefined, + "API should not be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testExplicitlyEnabled() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ], + }); + let value = Services.prefs.getBoolPref( + "dom.storage_access.forward_declared.enabled" + ); + ok(value, "dom.storage_access.forward_declared.enabled should be true"); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async _ => { + ok( + content.document.requestStorageAccessUnderSite != undefined, + "API should be on the window" + ); + ok( + content.document.completeStorageAccessRequestFromSite != undefined, + "API should be on the window" + ); + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js new file mode 100644 index 0000000000..a5cc6f10b5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_RequireIntermediatePermission.js @@ -0,0 +1,61 @@ +add_task(async function testIntermediatePermissionRequired() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(false, "Must not resolve."); + } catch { + ok(true, "Must reject because we don't have the initial request."); + } + }); + + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^https://example.org", + allow: 1, + context: TEST_TOP_PAGE, + }, + ]); + + await SpecialPowers.spawn(browser, [TEST_3RD_PARTY_DOMAIN], async tp => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok( + true, + "Must resolve now that we have the permission from the embedee." + ); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js new file mode 100644 index 0000000000..0b4f3e7273 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_StorageAccessPermission.js @@ -0,0 +1,94 @@ +add_task( + async function testStorageAccessPermissionRequestStorageAccessUnderSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_4TH_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "3rdPartyStorage^http://not-tracking.example.com", + allow: 1, + context: tp, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + + await BrowserTestUtils.removeTab(tab); + } +); + +add_task( + async function testStorageAccessPermissionCompleteStorageAccessRequestFromSite() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + [ + "network.cookie.cookieBehavior", + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_4TH_PARTY_DOMAIN], async tp => { + await SpecialPowers.pushPermissions([ + { + type: "AllowStorageAccessRequest^http://example.com", + allow: 1, + context: content.document, + }, + { + type: "3rdPartyStorage^http://not-tracking.example.com", + allow: 1, + context: content.document, + }, + ]); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve."); + } catch { + ok(false, "Must not reject."); + } + }); + await BrowserTestUtils.removeTab(tab); + } +); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js new file mode 100644 index 0000000000..49dd73fbf7 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_storageAccess_TopLevel_UserActivation.js @@ -0,0 +1,63 @@ +add_task(async function testUserActivations() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.forward_declared.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ["dom.storage_access.auto_grants", false], + ["dom.storage_access.max_concurrent_auto_grants", 1], + ], + }); + // Part 1: open the embedded site as a top level + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_3RD_PARTY_PAGE, + }); + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [TEST_DOMAIN], async tp => { + // Part 2: requestStorageAccessUnderSite without activation + var p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(false, "Must not resolve without user activation."); + } catch { + ok(true, "Must reject without user activation."); + } + // Part 3: requestStorageAccessUnderSite with activation + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + p = content.document.requestStorageAccessUnderSite(tp); + try { + await p; + ok(true, "Must resolve with user activation and autogrant."); + } catch { + ok(false, "Must not reject with user activation."); + } + }); + // Part 4: open the embedding site as a top level + let tab2 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_TOP_PAGE, + }); + let browser2 = tab2.linkedBrowser; + await SpecialPowers.spawn(browser2, [TEST_3RD_PARTY_DOMAIN], async tp => { + // Part 5: completeStorageAccessRequestFromSite without activation + var p = content.document.completeStorageAccessRequestFromSite(tp); + try { + await p; + ok(true, "Must resolve without user activation."); + } catch { + ok(false, "Must not reject without user activation in this context."); + } + }); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.removeTab(tab2); +}); + +add_task(async () => { + Services.perms.removeAll(); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResources.js b/toolkit/components/antitracking/test/browser/browser_subResources.js new file mode 100644 index 0000000000..4841527d19 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResources.js @@ -0,0 +1,277 @@ +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["privacy.partition.network_state", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading tracking scripts and tracking images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for images"); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "0", "Cookies received for scripts"); + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading tracking scripts and tracking images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "One cookie received for images."); + }); + + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + is(text, "1", "One cookie received received for scripts."); + }); + + let expectTrackerBlocked = (item, blocked) => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectTrackerFound = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectTrackerCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + is( + trackerOrigin + "/", + TEST_3RD_PARTY_DOMAIN, + "Correct tracker origin must be reported" + ); + let originLog = log[trackerOrigin]; + is(originLog.length, 5, "We should have 4 entries in the compressed log"); + expectTrackerFound(originLog[0]); + expectCookiesLoaded(originLog[1]); + expectTrackerCookiesLoaded(originLog[2]); + expectTrackerBlocked(originLog[3], true); + expectTrackerBlocked(originLog[4], false); + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js new file mode 100644 index 0000000000..b2de150075 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned.js @@ -0,0 +1,308 @@ +async function runTests(topPage, limitForeignContexts) { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading scripts and images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading scripts and images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + let expectTrackerBlocked = (item, blocked) => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, + "Correct blocking type reported" + ); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesBlockedForeign = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + let originLog = log[trackerOrigin]; + info(trackerOrigin); + switch (trackerOrigin) { + case "https://example.org": + case "https://example.com": + let numEntries = 1; + if (limitForeignContexts) { + ++numEntries; + } + is( + originLog.length, + numEntries, + `We should have ${numEntries} entries in the compressed log` + ); + expectCookiesLoaded(originLog[0]); + if (limitForeignContexts) { + expectCookiesBlockedForeign(originLog[1]); + } + break; + case "https://tracking.example.org": + is( + originLog.length, + 1, + "We should have 1 entries in the compressed log" + ); + expectTrackerBlocked(originLog[0], false); + break; + } + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "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], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + false, + ], + ], + }); + + for (let limitForeignContexts of [false, true]) { + SpecialPowers.setBoolPref( + "privacy.dynamic_firstparty.limitForeign", + limitForeignContexts + ); + for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) { + await runTests(page, limitForeignContexts); + } + } + + SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js new file mode 100644 index 0000000000..53a90854b3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_subResourcesPartitioned_alwaysPartition.js @@ -0,0 +1,313 @@ +async function runTests(topPage, limitForeignContexts) { + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, topPage); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Loading scripts and images"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.org/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_3RD_PARTY_PAGE_WO, + blockingCallback: (async _ => {}).toString(), + nonBlockingCallback: (async _ => {}).toString(), + }, + ], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Loading scripts and images again"); + await SpecialPowers.spawn(browser, [], async function () { + // Let's load the script twice here. + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + { + let src = content.document.createElement("script"); + let p = new content.Promise(resolve => { + src.onload = resolve; + }); + content.document.body.appendChild(src); + src.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=script"; + await p; + } + + // Let's load an image twice here. + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + { + let img = content.document.createElement("img"); + let p = new content.Promise(resolve => { + img.onload = resolve; + }); + content.document.body.appendChild(img); + img.src = + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?what=image"; + await p; + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=image" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received for images."); + } else { + is(text, "1", "One cookie received for images."); + } + }); + + await fetch( + "https://example.com/browser/toolkit/components/antitracking/test/browser/subResources.sjs?result&what=script" + ) + .then(r => r.text()) + .then(text => { + if (limitForeignContexts) { + is(text, "0", "No cookie received received for scripts."); + } else { + is(text, "1", "One cookie received received for scripts."); + } + }); + + let expectTrackerBlocked = (item, blocked, type) => { + is(item[0], type, "Correct blocking type reported"); + is(item[1], blocked, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesLoaded = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let expectCookiesBlockedForeign = item => { + is( + item[0], + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + "Correct blocking type reported" + ); + is(item[1], true, "Correct blocking status reported"); + ok(item[2] >= 1, "Correct repeat count reported"); + }; + + let log = JSON.parse(await browser.getContentBlockingLog()); + for (let trackerOrigin in log) { + let originLog = log[trackerOrigin]; + info(trackerOrigin); + switch (trackerOrigin) { + case "https://example.org": + case "https://example.com": + let numEntries = 1; + if (limitForeignContexts) { + ++numEntries; + } + is( + originLog.length, + numEntries, + `We should have ${numEntries} entries in the compressed log` + ); + expectCookiesLoaded(originLog[0]); + if (limitForeignContexts) { + expectCookiesBlockedForeign(originLog[1]); + } + break; + case "https://tracking.example.org": + is( + originLog.length, + 2, + "We should have 2 entries in the compressed log" + ); + expectTrackerBlocked( + originLog[0], + true, + Ci.nsIWebProgressListener.STATE_COOKIES_LOADED + ); + expectTrackerBlocked( + originLog[1], + false, + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER + ); + break; + } + } + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "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], + ["privacy.trackingprotection.annotate_channels", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ], + }); + + for (let limitForeignContexts of [false, true]) { + SpecialPowers.setBoolPref( + "privacy.dynamic_firstparty.limitForeign", + limitForeignContexts + ); + for (let page of [TEST_TOP_PAGE, TEST_TOP_PAGE_2, TEST_TOP_PAGE_3]) { + await runTests(page, limitForeignContexts); + } + } + + SpecialPowers.clearUserPref("privacy.dynamic_firstparty.limitForeign"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js new file mode 100644 index 0000000000..b415594662 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_thirdPartyStorageRejectionForCORS.js @@ -0,0 +1,96 @@ +// This test works by setting up an exception for the tracker domain, which +// disables all the anti-tracking tests. + +add_task(async _ => { + PermissionTestUtils.add( + "http://example.net", + "cookie", + Services.perms.ALLOW_ACTION + ); + + registerCleanupFunction(_ => { + Services.perms.removeAll(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we don't store 3P cookies from non-anonymous CORS XHR", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + blockingByContentBlockingRTUI: false, + allowList: false, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + callback: async _ => { + await new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open( + "GET", + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/cookiesCORS.sjs?some;max-age=999999", + true + ); + xhr.withCredentials = true; + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.onreadystatechange = _ => { + if (4 === xhr.readyState && 200 === xhr.status) { + resolve(); + } + }; + xhr.send(); + }); + }, + extraPrefs: [["network.cookie.rejectForeignWithExceptions.enabled", true]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); + +AntiTracking._createTask({ + name: "Test that we don't store 3P cookies from non-anonymous CORS XHR", + cookieBehavior: BEHAVIOR_REJECT_FOREIGN, + blockingByContentBlockingRTUI: false, + allowList: false, + thirdPartyPage: TEST_DOMAIN + TEST_PATH + "3rdParty.html", + callback: async _ => { + await new Promise(resolve => { + const xhr = new XMLHttpRequest(); + xhr.open( + "GET", + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/cookiesCORS.sjs?some;max-age=999999", + true + ); + xhr.withCredentials = true; + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.onreadystatechange = _ => { + if (4 === xhr.readyState && 200 === xhr.status) { + resolve(); + } + }; + xhr.send(); + }); + }, + extraPrefs: [["network.cookie.rejectForeignWithExceptions.enabled", false]], + expectedBlockingNotifications: + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js new file mode 100644 index 0000000000..642b5d2cbd --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping.js @@ -0,0 +1,253 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +const COLLECTION_NAME = "anti-tracking-url-decoration"; +const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations"; +const TOKEN_1 = "fooBar"; +const TOKEN_2 = "foobaz"; +const TOKEN_3 = "fooqux"; +const TOKEN_4 = "bazqux"; + +const token_1 = TOKEN_1.toLowerCase(); + +const DOMAIN = TEST_DOMAIN_3; +const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER = + SUB_DOMAIN + TEST_PATH + "page.html"; +const TOP_PAGE_WITH_TRACKING_IDENTIFIER = + TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123"; + +add_task(async _ => { + let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService( + Ci.nsIURLDecorationAnnotationsService + ); + + let records = [ + { + id: "1", + last_modified: 1000000000000001, + schema: Date.now(), + token: TOKEN_1, + }, + ]; + + // Add some initial data + async function emitSync() { + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + } + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), [records[0]]); + await emitSync(); + + await uds.ensureUpdated(); + + let list = Preferences.get(PREF_NAME).split(" "); + ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME); + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + + async function verifyList(array, not_array) { + await emitSync(); + + await uds.ensureUpdated(); + + list = Preferences.get(PREF_NAME).split(" "); + for (let token of array) { + ok( + list.includes(token), + token + " must now be available in " + PREF_NAME + ); + } + for (let token of not_array) { + ok( + !list.includes(token), + token + " must not be available in " + PREF_NAME + ); + } + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + } + + records.push( + { + id: "2", + last_modified: 1000000000000002, + schema: Date.now(), + token: TOKEN_2, + }, + { + id: "3", + last_modified: 1000000000000003, + schema: Date.now(), + token: TOKEN_3, + }, + { + id: "4", + last_modified: 1000000000000005, + schema: Date.now(), + token: TOKEN_4, + } + ); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []); + + records.pop(); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]); + + is( + Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)), + Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)), + "Sanity check" + ); + + registerCleanupFunction(async _ => { + records = []; + await db.clear(); + await emitSync(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + ok(ref.pathname.length > 1, "Path must not be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we downgrade document.referrer when it contains a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname should be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, false], + ], + expectedBlockingNotifications: 0, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js new file mode 100644 index 0000000000..2fbac9811b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlDecorationStripping_alwaysPartition.js @@ -0,0 +1,255 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +const trackerBlocked = Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); + +const COLLECTION_NAME = "anti-tracking-url-decoration"; +const PREF_NAME = "privacy.restrict3rdpartystorage.url_decorations"; +const TOKEN_1 = "fooBar"; +const TOKEN_2 = "foobaz"; +const TOKEN_3 = "fooqux"; +const TOKEN_4 = "bazqux"; + +const APS_PREF = + "privacy.partition.always_partition_third_party_non_cookie_storage"; + +const token_1 = TOKEN_1.toLowerCase(); + +const DOMAIN = TEST_DOMAIN_3; +const SUB_DOMAIN = "https://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER = + SUB_DOMAIN + TEST_PATH + "page.html"; +const TOP_PAGE_WITH_TRACKING_IDENTIFIER = + TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER + "?" + TOKEN_1 + "=123"; + +add_task(async _ => { + let uds = Cc["@mozilla.org/tracking-url-decoration-service;1"].getService( + Ci.nsIURLDecorationAnnotationsService + ); + + let records = [ + { + id: "1", + last_modified: 1000000000000001, + schema: Date.now(), + token: TOKEN_1, + }, + ]; + + // Add some initial data + async function emitSync() { + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + } + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), [records[0]]); + await emitSync(); + + await uds.ensureUpdated(); + + let list = Preferences.get(PREF_NAME).split(" "); + ok(list.includes(TOKEN_1), "Token must now be available in " + PREF_NAME); + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + + async function verifyList(array, not_array) { + await emitSync(); + + await uds.ensureUpdated(); + + list = Preferences.get(PREF_NAME).split(" "); + for (let token of array) { + ok( + list.includes(token), + token + " must now be available in " + PREF_NAME + ); + } + for (let token of not_array) { + ok( + !list.includes(token), + token + " must not be available in " + PREF_NAME + ); + } + ok(Preferences.locked(PREF_NAME), PREF_NAME + " must be locked"); + } + + records.push( + { + id: "2", + last_modified: 1000000000000002, + schema: Date.now(), + token: TOKEN_2, + }, + { + id: "3", + last_modified: 1000000000000003, + schema: Date.now(), + token: TOKEN_3, + }, + { + id: "4", + last_modified: 1000000000000005, + schema: Date.now(), + token: TOKEN_4, + } + ); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3, TOKEN_4], []); + + records.pop(); + + await verifyList([TOKEN_1, TOKEN_2, TOKEN_3], [TOKEN_4]); + + is( + Services.eTLD.getBaseDomain(Services.io.newURI(DOMAIN)), + Services.eTLD.getBaseDomain(Services.io.newURI(SUB_DOMAIN)), + "Sanity check" + ); + + registerCleanupFunction(async _ => { + records = []; + await db.clear(); + await emitSync(); + }); +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + ok(ref.pathname.length > 1, "Path must not be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we downgrade document.referrer when it contains a tracking identifier", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname should be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy", 3], // Ensure we don't downgrade because of the default policy. + ["network.http.referer.defaultPolicy.trackers", 3], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +AntiTracking._createTask({ + name: "Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more", + cookieBehavior: BEHAVIOR_REJECT_TRACKER, + blockingByContentBlockingRTUI: true, + allowList: false, + callback: async _ => { + let ref = new URL(document.referrer); + is( + ref.hostname, + "sub1.xn--hxajbheg2az3al.xn--jxalpdlp", + "Hostname shouldn't be stripped" + ); + is(ref.pathname.length, 1, "Path must be trimmed"); + // eslint-disable-next-line no-unused-vars + for (let entry of ref.searchParams.entries()) { + ok(false, "No query parameters should be found"); + } + }, + extraPrefs: [ + ["network.http.referer.defaultPolicy.trackers", 2], + [APS_PREF, true], + ], + expectedBlockingNotifications: trackerBlocked, + runInPrivateWindow: false, + iframeSandbox: null, + accessRemoval: null, + callbackAfterRemoval: null, + topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER, +}); + +add_task(async _ => { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js new file mode 100644 index 0000000000..6395110f41 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping.js @@ -0,0 +1,855 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +requestLongerTimeout(6); + +const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2; +const TEST_THIRD_PARTY_SUB_DOMAIN = "http://sub1.xn--exmple-cua.test/"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = + TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const TEST_CASES = [ + { testQueryString: "paramToStrip1=123", strippedQueryString: "" }, + { + testQueryString: "PARAMTOSTRIP1=123¶mToStrip2=456", + strippedQueryString: "", + }, + { + testQueryString: "paramToStrip1=123¶mToKeep=456", + strippedQueryString: "paramToKeep=456", + }, + { + testQueryString: "paramToStrip1=123¶mToKeep=456¶mToStrip2=abc", + strippedQueryString: "paramToKeep=456", + }, + { + testQueryString: "paramToKeep=123", + strippedQueryString: "paramToKeep=123", + }, + // Test to make sure we don't encode the unstripped parameters. + { + testQueryString: "paramToStrip1=123¶mToKeep=?$!%", + strippedQueryString: "paramToKeep=?$!%", + }, +]; + +let listService; + +function observeChannel(uri, expected) { + return TestUtils.topicObserved("http-on-modify-request", (subject, data) => { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let channelURI = channel.URI; + + if (channelURI.spec.startsWith(uri)) { + is( + channelURI.query, + expected, + "The loading channel has the expected query string." + ); + return true; + } + + return false; + }); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"], + ["privacy.query_stripping.redirect", true], + ["privacy.query_stripping.listService.logLevel", "Debug"], + ["privacy.query_stripping.strip_on_share.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +add_task(async function doTestsForTabOpen() { + info("Start testing query stripping for tab open."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testURI = TEST_URI + "?" + test.testQueryString; + + let expected = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + // Observe the channel and check if the query string is expected. + let networkPromise = observeChannel(TEST_URI, expected); + + // Open a new tab. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string is expected in the new tab. + await verifyQueryString(browser, expected); + }); + + await networkPromise; + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForWindowOpen() { + info("Start testing query stripping for window.open()."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when open + // a same-origin URI. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the opened tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_URI); + }); + + // Call window.open() to open the same-origin URI where the query string + // won't be stripped. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is expected in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, originalQueryString); + + BrowserTestUtils.removeTab(newTab); + + // Observe the channel and check if the query string is expected for + // cross-origin URI. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + // Call window.open() to open the cross-site URI where the query string + // could be stripped. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + newTab = await newTabPromise; + + // Verify if the query string is expected in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, expectedQueryString); + + BrowserTestUtils.removeTab(newTab); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForLinkClick() { + info("Start testing query stripping for link navigation."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // click a same-origin link. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, create a link to a cross-origin site to see if the query + // string is stripped as expected. + + // Observe the channel and check if the query string is expected when + // click a cross-origin link. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + // Create the promise to wait for the location change. + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Create a cross-origin link and click it to navigate. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForLinkClickInIframe() { + info("Start testing query stripping for link navigation in iframe."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is intact when + // click a same-origin link. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + testFirstPartyURI + ); + + // Create a same-site link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + let newOpenedTab = await newTabPromise; + + // Verify the query string in the content window. + await verifyQueryString( + newOpenedTab.linkedBrowser, + originalQueryString + ); + BrowserTestUtils.removeTab(newOpenedTab); + + // Second, create a link to a cross-origin site in the iframe to see if + // the query string is stripped as expected. + + // Observe the channel and check if the query string is expected when + // click a cross-origin link. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + // Create the promise to wait for the new tab. + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetURI); + + // Create a cross-origin link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + newOpenedTab = await newTabPromise; + + // Verify the query string in the content window. + await verifyQueryString( + newOpenedTab.linkedBrowser, + expectedQueryString + ); + BrowserTestUtils.removeTab(newOpenedTab); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForScriptNavigation() { + info("Start testing query stripping for script navigation."); + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // navigating to a same-origin URI via script. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, trigger a cross-origin navigation through script to see if + // the query string is stripped as expected. + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is expected. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForNoStrippingForIframeNavigation() { + info("Start testing no query stripping for iframe navigation."); + + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?" + test.testQueryString; + + // There should be no query stripping for the iframe navigation. + let originalQueryString = test.testQueryString; + let expectedQueryString = test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is intact when + // navigating an iframe. + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testFirstPartyURI + ); + + // Trigger the iframe navigation by script. + await SpecialPowers.spawn(iframeBC, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the iframe. + await verifyQueryString(iframeBC, originalQueryString); + + // Second, trigger a cross-origin navigation through script to see if + // the query string is still the same. + + let targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin iframe navigation by script. + await SpecialPowers.spawn(iframeBC, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(iframeBC, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestsForRedirect() { + info("Start testing query stripping for redirects."); + + for (const strippingEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", strippingEnabled]], + }); + await waitForListServiceInit(strippingEnabled); + + for (const test of TEST_CASES) { + let testFirstPartyURI = + TEST_REDIRECT_URI + "?" + TEST_URI + "?" + test.testQueryString; + let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${test.testQueryString}`; + + let originalQueryString = test.testQueryString; + let expectedQueryString = strippingEnabled + ? test.strippedQueryString + : test.testQueryString; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is intact when + // redirecting to a same-origin URI . + let networkPromise = observeChannel(TEST_URI, originalQueryString); + + let targetURI = `${TEST_URI}?${originalQueryString}`; + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testFirstPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, originalQueryString); + + // Second, trigger a redirect to a cross-origin site where the query + // string should be stripped. + + targetURI = expectedQueryString + ? `${TEST_THIRD_PARTY_URI}?${expectedQueryString}` + : TEST_THIRD_PARTY_URI; + + // Observe the channel and check if the query string is expected. + networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + expectedQueryString + ); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + targetURI + ); + + // Trigger the cross-origin redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expectedQueryString); + }); + } + + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function doTestForAllowList() { + info("Start testing query stripping allow list."); + + // Enable the query stripping and set the allow list. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.allow_list", "xn--exmple-cua.test"], + ], + }); + await waitForListServiceInit(true); + + const expected = "paramToStrip1=123"; + + // Make sure the allow list works for sites, so we will test both the domain + // and the sub domain. + for (const domain of [TEST_THIRD_PARTY_DOMAIN, TEST_THIRD_PARTY_SUB_DOMAIN]) { + let testURI = `${domain}${TEST_PATH}file_stripping.html`; + let testURIWithQueryString = `${testURI}?${expected}`; + + // 1. Test the allow list for tab open. + info("Run tab open test."); + + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + await BrowserTestUtils.withNewTab(testURIWithQueryString, async browser => { + // Verify if the query string is not stripped in the new tab. + await verifyQueryString(browser, expected); + }); + + await networkPromise; + + // 2. Test the allow list for window open + info("Run window open test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(testURI); + }); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + content.postMessage({ type: "window-open", url }, "*"); + } + ); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is not stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, expected); + + BrowserTestUtils.removeTab(newTab); + }); + + // 3. Test the allow list for link click + info("Run link click test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + let link = content.document.createElement("a"); + link.setAttribute("href", url); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + } + ); + + await networkPromise; + await locationChangePromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(browser, expected); + }); + + // 4. Test the allow list for clicking link in an iframe. + info("Run link click in iframe test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create an iframe and wait until it has been loaded. + let iframeBC = await SpecialPowers.spawn( + browser, + [TEST_URI], + async url => { + let frame = content.document.createElement("iframe"); + content.document.body.appendChild(frame); + + await new Promise(done => { + frame.addEventListener( + "load", + function () { + done(); + }, + { capture: true, once: true } + ); + + frame.setAttribute("src", url); + }); + + return frame.browsingContext; + } + ); + + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + testURIWithQueryString + ); + + // Create a same-site link which has '_blank' as target in the iframe + // and click it to navigate. + await SpecialPowers.spawn( + iframeBC, + [testURIWithQueryString], + async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.setAttribute("target", "_blank"); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + } + ); + + await networkPromise; + let newOpenedTab = await newTabPromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(newOpenedTab.linkedBrowser, expected); + BrowserTestUtils.removeTab(newOpenedTab); + + // 5. Test the allow list for script navigation. + info("Run script navigation test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + await SpecialPowers.spawn( + browser, + [testURIWithQueryString], + async url => { + content.postMessage({ type: "script", url }, "*"); + } + ); + + await networkPromise; + await locationChangePromise; + + // Verify the query string is not stripped in the content window. + await verifyQueryString(browser, expected); + }); + + // 6. Test the allow list for redirect. + info("Run redirect test."); + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel(testURI, expected); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURIWithQueryString + ); + + let testRedirectURI = `${TEST_REDIRECT_URI}?${testURI}?${expected}`; + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testRedirectURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expected); + }); + }); + } +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js new file mode 100644 index 0000000000..6dee6cede0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_allowList.js @@ -0,0 +1,442 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_THIRD_PARTY_DOMAIN = TEST_DOMAIN_2; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = + TEST_THIRD_PARTY_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const TEST_QUERY_STRING = "paramToStrip=1"; + +function observeChannel(uri, expected) { + return TestUtils.topicObserved("http-on-before-connect", (subject, data) => { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + let channelURI = channel.URI; + + if (channelURI.spec.startsWith(uri)) { + is( + channelURI.query, + expected, + "The loading channel has the expected query string." + ); + return true; + } + + return false; + }); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip"], + ["privacy.query_stripping.redirect", true], + ["privacy.query_stripping.enabled", true], + ], + }); + + let listService = Cc[ + "@mozilla.org/query-stripping-list-service;1" + ].getService(Ci.nsIURLQueryStrippingListService); + await listService.testWaitForInit(); +}); + +add_task(async function doTestsForTabOpen() { + let testURI = TEST_URI + "?" + TEST_QUERY_STRING; + + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_URI, ""); + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI); + + // Verify if the query string is stripped. + await verifyQueryString(tab.linkedBrowser, ""); + await networkPromise; + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + BrowserTestUtils.removeTab(tab); + + // Open the tab again and check if the query string is not stripped. + networkPromise = observeChannel(TEST_URI, TEST_QUERY_STRING); + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURI); + await networkPromise; + + // Verify if the query string is not stripped because it's in the content + // blocking allow list. + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + TEST_URI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, ""); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function doTestsForWindowOpen() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the opened tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + // Call window.open() to open the third-party URI. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + let newTab = await newTabPromise; + + // Verify if the query string is stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING); + + BrowserTestUtils.removeTab(newTab); + + // Call window.open() again to check if the query string is not stripped if + // it's in the content blocking allow list. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url => { + return url.startsWith(TEST_THIRD_PARTY_URI); + }); + + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "window-open", url }, "*"); + }); + + await networkPromise; + newTab = await newTabPromise; + + // Verify if the query string is not stripped in the new opened tab. + await verifyQueryString(newTab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + TEST_THIRD_PARTY_URI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(newTab.linkedBrowser, ""); + BrowserTestUtils.removeTab(newTab); + }); +}); + +add_task(async function doTestsForLinkClick() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + }); + + // Repeat the test again to see if the query string is not stripped if it's in + // the content blocking allow list. + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + TEST_QUERY_STRING + ); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + TEST_THIRD_PARTY_URI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + }); +}); + +add_task(async function doTestsForScriptNavigation() { + let testURI = TEST_THIRD_PARTY_URI + "?" + TEST_QUERY_STRING; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + testURI + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + }); + + // Repeat the test again to see if the query string is not stripped if it's in + // the content blocking allow list. + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Observe the channel and check if the query string is not stripped. + let networkPromise = observeChannel( + TEST_THIRD_PARTY_URI, + TEST_QUERY_STRING + ); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + TEST_THIRD_PARTY_URI + ); + // Observe the channel and check if the query string is stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, ""); + }); +}); + +add_task(async function doTestsForRedirect() { + let testURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`; + let resultURI = TEST_THIRD_PARTY_URI; + let resultURIWithQuery = `${TEST_THIRD_PARTY_URI}?${TEST_QUERY_STRING}`; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + + // Observe the channel and check if the query string is stripped. + let networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + resultURI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(tab.linkedBrowser, ""); + + // Toggle ETP off and verify if the query string is restored. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + resultURIWithQuery + ); + // Observe the channel and check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + + gProtectionsHandler.disableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + BrowserTestUtils.removeTab(tab); + + // Open the tab again to check if the query string is not stripped. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, TEST_QUERY_STRING); + tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URI); + + locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + resultURIWithQuery + ); + + // Trigger the redirect. + await SpecialPowers.spawn(tab.linkedBrowser, [testURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await networkPromise; + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(tab.linkedBrowser, TEST_QUERY_STRING); + + // Toggle ETP on and verify if the query string is stripped again. + networkPromise = observeChannel(TEST_THIRD_PARTY_URI, ""); + browserLoadedPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + resultURI + ); + gProtectionsHandler.enableForCurrentPage(); + await browserLoadedPromise; + await networkPromise; + + await verifyQueryString(tab.linkedBrowser, ""); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js new file mode 100644 index 0000000000..e5f256b870 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_nimbus.js @@ -0,0 +1,145 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Simplified version of browser_urlQueryStripping.js to test that the Nimbus + * integration works correctly in both normal and private browsing. + */ + +const { ExperimentFakes } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_QUERY_STRING = "paramToStrip1=123¶mToKeep=456"; +const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456"; +const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING; + +let listService; + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +/** + * Set a list of prefs on the default branch and restore the original values on test end. + * @param {*} prefs - Key value pairs in an array. + */ +function setDefaultPrefs(prefs) { + let originalValues = new Map(); + let defaultPrefs = Services.prefs.getDefaultBranch(""); + + let prefValueToSetter = prefValue => { + let type = typeof prefValue; + if (type == "string") { + return defaultPrefs.setStringPref; + } + if (type == "boolean") { + return defaultPrefs.setBoolPref; + } + throw new Error("unexpected pref type"); + }; + + prefs.forEach(([key, value]) => { + prefValueToSetter(value)(key, value); + originalValues.set(key, value); + }); + + registerCleanupFunction(function () { + prefs.forEach(([key, value]) => { + prefValueToSetter(value)(key, originalValues.get(key)); + }); + }); +} + +add_setup(async function () { + // Disable the feature via the default pref. This is required so we can set + // user values via Nimbus. + setDefaultPrefs([ + ["privacy.query_stripping.enabled", false], + ["privacy.query_stripping.enabled.pbmode", false], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.strip_on_share.enabled", false], + ]); + + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.listService.logLevel", "Debug"]], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +add_task(async function test() { + let [normalWindow, pbWindow] = await Promise.all([ + BrowserTestUtils.openNewBrowserWindow(), + BrowserTestUtils.openNewBrowserWindow({ private: true }), + ]); + + for (let enableStripPBM of [false, true]) { + for (let enableStrip of [false, true]) { + let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "queryStripping", + value: { + enabledNormalBrowsing: enableStrip, + enabledPrivateBrowsing: enableStripPBM, + stripList: "paramToStrip1 paramToStrip2", + }, + }); + + for (let testPBM of [false, true]) { + let shouldStrip = + (testPBM && enableStripPBM) || (!testPBM && enableStrip); + let expectedQueryString = shouldStrip + ? TEST_QUERY_STRING_STRIPPED + : TEST_QUERY_STRING; + + info( + "Test stripping " + + JSON.stringify({ + enableStripPBM, + enableStrip, + testPBM, + expectedQueryString, + }) + ); + + await waitForListServiceInit(enableStripPBM || enableStrip); + + let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser; + await BrowserTestUtils.withNewTab( + { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY }, + async browser => { + is( + browser.currentURI.query, + expectedQueryString, + "Correct query string" + ); + } + ); + } + + await doExperimentCleanup(); + } + } + + // Cleanup + await Promise.all([ + BrowserTestUtils.closeWindow(normalWindow), + BrowserTestUtils.closeWindow(pbWindow), + ]); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js new file mode 100644 index 0000000000..fd37f94765 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_pbmode.js @@ -0,0 +1,105 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Simplified version of browser_urlQueryStripping.js to test that the feature + * prefs work correctly in both normal and private browsing. + */ + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_QUERY_STRING = "paramToStrip1=123¶mToKeep=456"; +const TEST_QUERY_STRING_STRIPPED = "paramToKeep=456"; +const TEST_URI_WITH_QUERY = TEST_URI + "?" + TEST_QUERY_STRING; + +let listService; + +async function waitForListServiceInit(strippingEnabled) { + info("Waiting for nsIURLQueryStrippingListService to be initialized."); + let isInitialized = await listService.testWaitForInit(); + is( + isInitialized, + strippingEnabled, + "nsIURLQueryStrippingListService should be initialized when the feature is enabled." + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_list", "paramToStrip1 paramToStrip2"], + ["privacy.query_stripping.listService.logLevel", "Debug"], + ["privacy.query_stripping.strip_on_share.enabled", false], + ], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + // Here we don't care about the actual enabled state, we just want any init to be done so we get reliable starting conditions. + await listService.testWaitForInit(); +}); + +add_task(async function test() { + let [normalWindow, pbWindow] = await Promise.all([ + BrowserTestUtils.openNewBrowserWindow(), + BrowserTestUtils.openNewBrowserWindow({ private: true }), + ]); + + for (let enableStripPBM of [false, true]) { + Services.prefs.setBoolPref( + "privacy.query_stripping.enabled.pbmode", + enableStripPBM + ); + for (let enableStrip of [false, true]) { + Services.prefs.setBoolPref( + "privacy.query_stripping.enabled", + enableStrip + ); + for (let testPBM of [false, true]) { + let shouldStrip = + (testPBM && enableStripPBM) || (!testPBM && enableStrip); + let expectedQueryString = shouldStrip + ? TEST_QUERY_STRING_STRIPPED + : TEST_QUERY_STRING; + + info( + "Test stripping " + + JSON.stringify({ + enableStripPBM, + enableStrip, + testPBM, + expectedQueryString, + }) + ); + + await waitForListServiceInit(enableStripPBM || enableStrip); + + let tabBrowser = testPBM ? pbWindow.gBrowser : normalWindow.gBrowser; + await BrowserTestUtils.withNewTab( + { gBrowser: tabBrowser, url: TEST_URI_WITH_QUERY }, + async browser => { + is( + browser.currentURI.query, + expectedQueryString, + "Correct query string" + ); + } + ); + } + } + } + + // Cleanup + await Promise.all([ + BrowserTestUtils.closeWindow(normalWindow), + BrowserTestUtils.closeWindow(pbWindow), + ]); + + Services.prefs.clearUserPref("privacy.query_stripping.enabled"); + Services.prefs.clearUserPref("privacy.query_stripping.enabled.pbmode"); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js new file mode 100644 index 0000000000..20c830113c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry.js @@ -0,0 +1,355 @@ +/** + * Bug 1706616 - Testing the URL query string stripping telemetry. + */ + +"use strict"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; +const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "file_stripping.html"; +const TEST_REDIRECT_URI = TEST_DOMAIN + TEST_PATH + "redirect.sjs"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const LABEL_NAVIGATION = 0; +const LABEL_REDIRECT = 1; +const LABEL_STRIP_FOR_NAVIGATION = 2; +const LABEL_STRIP_FOR_REDIRECT = 3; + +const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT"; +const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT"; + +async function clearTelemetry() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their data with the parent process, we wait + // this out to ensure that we clear everything. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear(); + + let isCleared = () => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + + return ( + !histograms || + (!histograms[QUERY_STRIPPING_COUNT] && + !histograms[QUERY_STRIPPING_PARAM_COUNT]) + ); + }; + + // Check that the telemetry probes have been cleared properly. Do this check + // sync first to avoid any race conditions where telemetry arrives after + // clearing. + if (!isCleared()) { + await TestUtils.waitForCondition( + isCleared, + "waiting for query stripping probes to be cleared" + ); + } + + ok(true, "Telemetry has been cleared."); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +async function getTelemetryProbe(key, label, checkCntFn) { + let histogram; + + // Wait until the telemetry probe appears. + await TestUtils.waitForCondition(() => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).parent; + histogram = histograms[key]; + + let checkRes = false; + + if (histogram) { + checkRes = checkCntFn ? checkCntFn(histogram.values[label]) : true; + } + + return checkRes; + }, `waiting for telemetry probe (key=${key}, label=${label}) to appear`); + + return histogram.values[label]; +} + +async function checkTelemetryProbe(key, expectedCnt, label) { + let cnt = await getTelemetryProbe(key, label, cnt => cnt == expectedCnt); + + is(cnt, expectedCnt, "There should be expected count in telemetry."); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + [ + "privacy.query_stripping.strip_list", + "paramToStrip paramToStripB paramToStripC paramToStripD", + ], + ], + }); + + // Clear Telemetry probes before testing. + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInParent() { + let testURI = TEST_URI + "?paramToStrip=value"; + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + // Because there would be some loading happening during the test and they + // could interfere the count here. So, we only verify if the counter is + // increased, but not the exact count. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInContent() { + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingNavigationInContentQueryCount() { + let testThirdPartyURI = + TEST_THIRD_PARTY_URI + + "?paramToStrip=value¶mToStripB=valueB¶mToStripC=valueC¶mToStripD=valueD"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + 1, + LABEL_STRIP_FOR_NAVIGATION + ); + + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "0", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "1", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "2", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "3", cnt => !cnt); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "4", cnt => cnt == 1); + await getTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, "5", cnt => !cnt); + + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingRedirect() { + let testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_THIRD_PARTY_URI + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, ""); + }); + + // Verify the telemetry probe in parent process. Note that there is no + // non-test loading is using redirect. So, we can check the exact count here. + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_STRIP_FOR_REDIRECT); + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT); + await checkTelemetryProbe(QUERY_STRIPPING_PARAM_COUNT, 1, "1"); + + await clearTelemetry(); +}); + +add_task(async function testQueryStrippingDisabled() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.enabled", false]], + }); + + // First, test the navigation in parent process. + let testURI = TEST_URI + "?paramToStrip=value"; + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string was not happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe. There should be no stripped navigation count. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_NAVIGATION + ); + // Check if the navigation count is increased. + let newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + // Second, test the navigation in content. + let testThirdPartyURI = TEST_THIRD_PARTY_URI + "?paramToStrip=value"; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + testThirdPartyURI + ); + + // Trigger the navigation by script. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe in content process. There should be no stripped + // navigation count. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_NAVIGATION + ); + // Check if the navigation count is increased. + newNavigationCnt = await getTelemetryProbe( + QUERY_STRIPPING_COUNT, + LABEL_NAVIGATION, + cnt => cnt > 0 + ); + ok(newNavigationCnt > 0, "There is navigation count added."); + + // Third, test the redirect. + testThirdPartyURI = `${TEST_REDIRECT_URI}?${TEST_THIRD_PARTY_URI}?paramToStrip=value`; + + await BrowserTestUtils.withNewTab(TEST_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + `${TEST_THIRD_PARTY_URI}?paramToStrip=value` + ); + + // Trigger the redirect. + await SpecialPowers.spawn(browser, [testThirdPartyURI], async url => { + content.postMessage({ type: "script", url }, "*"); + }); + + await locationChangePromise; + + // Verify if the query string was happened. + await verifyQueryString(browser, "paramToStrip=value"); + }); + + // Verify the telemetry probe. The stripped redirect count should not exist. + await checkTelemetryProbe( + QUERY_STRIPPING_COUNT, + undefined, + LABEL_STRIP_FOR_REDIRECT + ); + await checkTelemetryProbe(QUERY_STRIPPING_COUNT, 1, LABEL_REDIRECT); + + await clearTelemetry(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js new file mode 100644 index 0000000000..6874e4bc62 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStringStripping_telemetry_2.js @@ -0,0 +1,159 @@ +"use strict"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "file_stripping.html"; + +const QUERY_STRIPPING_COUNT = "QUERY_STRIPPING_COUNT"; +const QUERY_STRIPPING_PARAM_COUNT = "QUERY_STRIPPING_PARAM_COUNT"; +const QUERY_STRIPPING_COUNT_BY_PARAM = "QUERY_STRIPPING_COUNT_BY_PARAM"; + +const histogramLabels = + Services.telemetry.getCategoricalLabels().QUERY_STRIPPING_COUNT_BY_PARAM; + +async function clearTelemetry() { + // There's an arbitrary interval of 2 seconds in which the content + // processes sync their data with the parent process, we wait + // this out to ensure that we clear everything. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 2000)); + + Services.telemetry.getSnapshotForHistograms("main", true /* clear */); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_PARAM_COUNT).clear(); + Services.telemetry.getHistogramById(QUERY_STRIPPING_COUNT_BY_PARAM).clear(); + + let isCleared = () => { + let histograms = Services.telemetry.getSnapshotForHistograms( + "main", + false /* clear */ + ).content; + + return ( + !histograms || + (!histograms[QUERY_STRIPPING_COUNT] && + !histograms[QUERY_STRIPPING_PARAM_COUNT] && + !histograms.QUERY_STRIPPING_COUNT_BY_PARAM) + ); + }; + + // Check that the telemetry probes have been cleared properly. Do this check + // sync first to avoid any race conditions where telemetry arrives after + // clearing. + if (!isCleared()) { + await TestUtils.waitForCondition(isCleared); + } + + ok(true, "Telemetry has been cleared."); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +function testTelemetry(queryParamToCount) { + const histogram = Services.telemetry.getHistogramById( + QUERY_STRIPPING_COUNT_BY_PARAM + ); + + let snapshot = histogram.snapshot(); + + let indexToCount = {}; + Object.entries(queryParamToCount).forEach(([key, value]) => { + let index = histogramLabels.indexOf(`param_${key}`); + + // In debug builds we perform additional stripping for testing, which + // results in telemetry being recorded twice. This does not impact + // production builds. + if (SpecialPowers.isDebugBuild) { + indexToCount[index] = value * 2; + } else { + indexToCount[index] = value; + } + }); + + for (let [i, val] of Object.entries(snapshot.values)) { + let expectedCount = indexToCount[i] || 0; + + is( + val, + expectedCount, + `Histogram ${QUERY_STRIPPING_COUNT_BY_PARAM} should have expected value for label ${histogramLabels[i]}.` + ); + } +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + [ + "privacy.query_stripping.strip_list", + "foo mc_eid oly_anon_id oly_enc_id __s vero_id _hsenc mkt_tok fbclid", + ], + ], + }); + + // Clear Telemetry probes before testing. + await clearTelemetry(); +}); + +/** + * Tests the QUERY_STRIPPING_COUNT_BY_PARAM histogram telemetry which counts how + * often query params from a predefined lists are stripped. + */ +add_task(async function test_queryParamCountTelemetry() { + info("Test with a query params to be stripped and recoded in telemetry."); + let url = new URL(TEST_URI); + url.searchParams.set("mc_eid", "myValue"); + + // Open a new tab and trigger the query stripping. + await BrowserTestUtils.withNewTab(url.href, async browser => { + // Verify that the tracking query param has been stripped. + await verifyQueryString(browser, ""); + }); + + testTelemetry({ mc_eid: 1 }); + + // Repeat this with the same query parameter, the respective histogram bucket + // should be incremented. + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + + testTelemetry({ mc_eid: 2 }); + + url = new URL(TEST_URI); + url.searchParams.set("fbclid", "myValue2"); + url.searchParams.set("mkt_tok", "myValue3"); + url.searchParams.set("bar", "foo"); + + info("Test with multiple query params to be stripped."); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, "bar=foo"); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1 }); + + info( + "Test with query param on the strip-list, which should not be recoded in telemetry." + ); + url = new URL(TEST_URI); + url.searchParams.set("foo", "bar"); + url.searchParams.set("__s", "myValue4"); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1 }); + + url = new URL(TEST_URI); + url.searchParams.set("foo", "bar"); + await BrowserTestUtils.withNewTab(url.href, async browser => { + await verifyQueryString(browser, ""); + }); + testTelemetry({ mc_eid: 2, fbclid: 1, mkt_tok: 1, __s: 1 }); + + await clearTelemetry(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js new file mode 100644 index 0000000000..1d46986ff1 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_urlQueryStrippingListService.js @@ -0,0 +1,249 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "urlQueryStrippingListService", + "@mozilla.org/query-stripping-list-service;1", + "nsIURLQueryStrippingListService" +); + +const COLLECTION_NAME = "query-stripping"; + +const TEST_URI = TEST_DOMAIN + TEST_PATH + "empty.html"; +const TEST_THIRD_PARTY_URI = TEST_DOMAIN_2 + TEST_PATH + "empty.html"; + +// The Update Event here is used to listen the observer from the +// URLQueryStrippingListService. We need to use the event here so that the same +// observer can be called multiple times. +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return BrowserTestUtils.waitForEvent(element, eventName).then(e => e.detail); +} + +async function verifyQueryString(browser, expected) { + await SpecialPowers.spawn(browser, [expected], expected => { + // Strip the first question mark. + let search = content.location.search.slice(1); + + is(search, expected, "The query string is correct."); + }); +} + +async function check(query, expected) { + // Open a tab with the query string. + let testURI = TEST_URI + "?" + query; + + // Test for stripping in parent process. + await BrowserTestUtils.withNewTab(testURI, async browser => { + // Verify if the query string is expected in the new tab. + await verifyQueryString(browser, expected); + }); + + testURI = TEST_URI + "?" + query; + let expectedURI; + if (expected != "") { + expectedURI = TEST_URI + "?" + expected; + } else { + expectedURI = TEST_URI; + } + + // Test for stripping in content processes. This will first open a third-party + // page and create a link to the test uri. And then, click the link to + // navigate the page, which will trigger the stripping in content processes. + await BrowserTestUtils.withNewTab(TEST_THIRD_PARTY_URI, async browser => { + // Create the promise to wait for the location change. + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + expectedURI + ); + + // Create a link and click it to navigate. + await SpecialPowers.spawn(browser, [testURI], async uri => { + let link = content.document.createElement("a"); + link.setAttribute("href", uri); + link.textContent = "Link"; + content.document.body.appendChild(link); + link.click(); + }); + + await locationChangePromise; + + // Verify the query string in the content window. + await verifyQueryString(browser, expected); + }); +} + +registerCleanupFunction(() => { + Cc["@mozilla.org/query-stripping-list-service;1"] + .getService(Ci.nsIURLQueryStrippingListService) + .clearLists(); +}); + +add_task(async function testPrefSettings() { + // Enable query stripping and clear the prefs at the beginning. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.allow_list", ""], + ["privacy.query_stripping.testing", true], + ], + }); + + // Test if the observer been called when adding to the service. + let updateEvent = new UpdateEvent(); + let obs = (stripList, allowList) => { + let event = new CustomEvent("update", { detail: { stripList, allowList } }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(updateEvent, "update"); + urlQueryStrippingListService.registerAndRunObserver(obs); + let lists = await promise; + is(lists.stripList, "", "No strip list at the beginning."); + is(lists.allowList, "", "No allow list at the beginning."); + + // Verify that no query stripping happens. + await check("pref_query1=123", "pref_query1=123"); + await check("pref_query2=456", "pref_query2=456"); + + // Set pref for strip list + promise = waitForEvent(updateEvent, "update"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_list", "pref_query1 pref_query2"]], + }); + lists = await promise; + + is( + lists.stripList, + "pref_query1 pref_query2", + "There should be strip list entries." + ); + is(lists.allowList, "", "There should be no allow list entries."); + + // The query string should be stripped. + await check("pref_query1=123", ""); + await check("pref_query2=456", ""); + + // Set the pref for allow list. + promise = waitForEvent(updateEvent, "update"); + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.allow_list", "example.net"]], + }); + lists = await promise; + + is( + lists.stripList, + "pref_query1 pref_query2", + "There should be strip list entires." + ); + is(lists.allowList, "example.net", "There should be one allow list entry."); + + // The query string shouldn't be stripped because this host is in allow list. + await check("pref_query1=123", "pref_query1=123"); + await check("pref_query2=123", "pref_query2=123"); + + urlQueryStrippingListService.unregisterObserver(obs); + + // Clear prefs. + SpecialPowers.flushPrefEnv(); +}); + +add_task(async function testRemoteSettings() { + // Enable query stripping and clear the prefs at the beginning. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.enabled", true], + ["privacy.query_stripping.strip_list", ""], + ["privacy.query_stripping.allow_list", ""], + ["privacy.query_stripping.testing", true], + ], + }); + + // Add initial empty record. + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), []); + + // Test if the observer been called when adding to the service. + let updateEvent = new UpdateEvent(); + let obs = (stripList, allowList) => { + let event = new CustomEvent("update", { detail: { stripList, allowList } }); + updateEvent.dispatchEvent(event); + }; + let promise = waitForEvent(updateEvent, "update"); + urlQueryStrippingListService.registerAndRunObserver(obs); + let lists = await promise; + is(lists.stripList, "", "No strip list at the beginning."); + is(lists.allowList, "", "No allow list at the beginning."); + + // Verify that no query stripping happens. + await check("remote_query1=123", "remote_query1=123"); + await check("remote_query2=456", "remote_query2=456"); + + // Set record for strip list. + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "1", + last_modified: 1000000000000001, + stripList: ["remote_query1", "remote_query2"], + allowList: [], + }, + ], + }, + }); + lists = await promise; + + is( + lists.stripList, + "remote_query1 remote_query2", + "There should be strip list entries." + ); + is(lists.allowList, "", "There should be no allow list entries."); + + // The query string should be stripped. + await check("remote_query1=123", ""); + await check("remote_query2=456", ""); + + // Set record for strip list and allow list. + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { + current: [ + { + id: "2", + last_modified: 1000000000000002, + stripList: ["remote_query1", "remote_query2"], + allowList: ["example.net"], + }, + ], + }, + }); + lists = await promise; + + is( + lists.stripList, + "remote_query1 remote_query2", + "There should be strip list entries." + ); + is(lists.allowList, "example.net", "There should be one allow list entry."); + + // The query string shouldn't be stripped because this host is in allow list. + await check("remote_query1=123", "remote_query1=123"); + await check("remote_query2=123", "remote_query2=123"); + + urlQueryStrippingListService.unregisterObserver(obs); + + // Clear the remote settings. + await db.clear(); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_userInteraction.js b/toolkit/components/antitracking/test/browser/browser_userInteraction.js new file mode 100644 index 0000000000..d343a56731 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_userInteraction.js @@ -0,0 +1,124 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userInteraction.document.interval", 1], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let uri = Services.io.newURI(TEST_DOMAIN); + is( + PermissionTestUtils.testPermission(uri, "storageAccessAPI"), + Services.perms.UNKNOWN_ACTION, + "Before user-interaction we don't have a permission" + ); + + let promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + return ( + permission.type == "storageAccessAPI" && + permission.principal.equalsURI(uri) + ); + }); + + info("Simulating user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + + // Let's see if the document is able to update the permission correctly. + for (var i = 0; i < 3; ++i) { + // Another perm-changed event should be triggered by the timer. + promise = TestUtils.topicObserved("perm-changed", (aSubject, aData) => { + let permission = aSubject.QueryInterface(Ci.nsIPermission); + return ( + permission.type == "storageAccessAPI" && + permission.principal.equalsURI(uri) + ); + }); + + info("Simulating another user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + } + + // Let's disable the document.interval. + await SpecialPowers.pushPrefEnv({ + set: [["privacy.userInteraction.document.interval", 0]], + }); + + promise = new Promise(resolve => { + let id; + + function observer(subject, topic, data) { + ok(false, "Notification received!"); + Services.obs.removeObserver(observer, "perm-changed"); + clearTimeout(id); + resolve(); + } + + Services.obs.addObserver(observer, "perm-changed"); + + id = setTimeout(() => { + ok(true, "No notification received!"); + Services.obs.removeObserver(observer, "perm-changed"); + resolve(); + }, 2000); + }); + + info("Simulating another user-interaction."); + await SpecialPowers.spawn(browser, [], async function () { + content.document.userInteractionForTesting(); + }); + + info("Waiting to have a permissions set."); + await promise; + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/browser_workerPropagation.js b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js new file mode 100644 index 0000000000..54ec0c1bf6 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/browser_workerPropagation.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +add_task(async function () { + info("Starting subResources test"); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "tracking.example.com,tracking.example.org", + ], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's create an iframe and run the test there. + let page = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "workerIframe.html"; + await SpecialPowers.spawn(browser, [page], async function (page) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.id = "test"; + + content.addEventListener("message", e => { + if (e.data.type == "finish") { + resolve(); + return; + } + + if (e.data.type == "info") { + info(e.data.msg); + return; + } + + if (e.data.type == "ok") { + ok(e.data.what, e.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = page; + }); + }); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +add_task(async function () { + info("Cleaning up."); + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/clearSiteData.sjs b/toolkit/components/antitracking/test/browser/clearSiteData.sjs new file mode 100644 index 0000000000..374f03a474 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/clearSiteData.sjs @@ -0,0 +1,6 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Clear-Site-Data", '"*"'); + aResponse.setHeader("Content-Type", "text/plain"); + aResponse.write("Clear-Site-Data"); +} diff --git a/toolkit/components/antitracking/test/browser/container.html b/toolkit/components/antitracking/test/browser/container.html new file mode 100644 index 0000000000..24daa80113 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/container.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<iframe src="embedder.html"></iframe> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/container2.html b/toolkit/components/antitracking/test/browser/container2.html new file mode 100644 index 0000000000..c2591ad7fc --- /dev/null +++ b/toolkit/components/antitracking/test/browser/container2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<body> +<script> + onmessage = function(e) { + parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="embedder2.html"></iframe> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/cookies.sjs b/toolkit/components/antitracking/test/browser/cookies.sjs new file mode 100644 index 0000000000..1267a69d8c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/cookies.sjs @@ -0,0 +1,12 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + let cookie = ""; + if (aRequest.hasHeader("Cookie")) { + cookie = aRequest.getHeader("Cookie"); + } + aResponse.write("cookie:" + cookie); + + if (aRequest.queryString) { + aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString); + } +} diff --git a/toolkit/components/antitracking/test/browser/cookiesCORS.sjs b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs new file mode 100644 index 0000000000..2cfdca2700 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/cookiesCORS.sjs @@ -0,0 +1,9 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Access-Control-Allow-Origin", "http://example.net"); + aResponse.setHeader("Access-Control-Allow-Credentials", "true"); + + if (aRequest.queryString) { + aResponse.setHeader("Set-Cookie", "foopy=" + aRequest.queryString); + } +} diff --git a/toolkit/components/antitracking/test/browser/dedicatedWorker.js b/toolkit/components/antitracking/test/browser/dedicatedWorker.js new file mode 100644 index 0000000000..72fd4ad850 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/dedicatedWorker.js @@ -0,0 +1,3 @@ +self.onmessage = msg => { + self.postMessage(msg.data); +}; diff --git a/toolkit/components/antitracking/test/browser/dynamicfpi_head.js b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js new file mode 100644 index 0000000000..6eaa620508 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/dynamicfpi_head.js @@ -0,0 +1,180 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from head.js */ + +"use strict"; + +this.DynamicFPIHelper = { + getTestPageConfig(runInSecureContext) { + if (runInSecureContext) { + return { + topPage: TEST_TOP_PAGE_HTTPS, + thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE_HTTPS, + partitionKey: "(https,example.net)", + }; + } + return { + topPage: TEST_TOP_PAGE, + thirdPartyPage: TEST_4TH_PARTY_STORAGE_PAGE, + partitionKey: "(http,example.net)", + }; + }, + + runTest( + name, + callback, + cleanupFunction, + extraPrefs, + runInPrivateWindow, + { runInSecureContext = false } = {} + ) { + add_task(async _ => { + info( + "Starting test `" + + name + + "' with dynamic FPI running in a " + + (runInPrivateWindow ? "private" : "normal") + + " window..." + ); + + await SpecialPowers.flushPrefEnv(); + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + runInPrivateWindow + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["privacy.dynamic_firstparty.use_site", true], + ["dom.security.https_first_pbm", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "not-tracking.example.com", + ], + ], + }); + + if (extraPrefs && Array.isArray(extraPrefs) && extraPrefs.length) { + await SpecialPowers.pushPrefEnv({ set: extraPrefs }); + } + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + const { topPage, thirdPartyPage, partitionKey } = + this.getTestPageConfig(runInSecureContext); + + info("Creating a new tab"); + let tab = BrowserTestUtils.addTab(win.gBrowser, topPage); + win.gBrowser.selectedTab = tab; + + let browser = win.gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + info("Check the cookieJarSettings of the browser object"); + ok( + browser.cookieJarSettings, + "The browser object has the cookieJarSettings." + ); + is( + browser.cookieJarSettings.cookieBehavior, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + "The cookieJarSettings has the correct cookieBehavior" + ); + is( + browser.cookieJarSettings.partitionKey, + partitionKey, + "The cookieJarSettings has the correct partitionKey" + ); + + info("Creating a 3rd party content"); + await SpecialPowers.spawn( + browser, + [ + { + page: thirdPartyPage, + callback: callback.toString(), + partitionKey, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = async _ => { + await SpecialPowers.spawn(ifr, [obj], async obj => { + is( + content.document.nodePrincipal.originAttributes.partitionKey, + "", + "We don't have first-party set on nodePrincipal" + ); + is( + content.document.effectiveStoragePrincipal.originAttributes + .partitionKey, + obj.partitionKey, + "We have first-party set on storagePrincipal" + ); + }); + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); + + if (runInPrivateWindow) { + win.close(); + } + }); + + add_task(async _ => { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + /* import-globals-from antitracking_head.js */ + forceGC(); + }); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/embedder.html b/toolkit/components/antitracking/test/browser/embedder.html new file mode 100644 index 0000000000..1a517079e0 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/embedder.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script> diff --git a/toolkit/components/antitracking/test/browser/embedder2.html b/toolkit/components/antitracking/test/browser/embedder2.html new file mode 100644 index 0000000000..b1441894a4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/embedder2.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect"></script> +<script src="https://tracking.example.com/browser/toolkit/components/antitracking/test/browser/empty.js?redirect2"></script> +</head> +<body onload="parent.postMessage({data:document.querySelectorAll('script').length}, '*');"></body> +</html> diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js b/toolkit/components/antitracking/test/browser/empty-altsvc.js new file mode 100644 index 0000000000..3053583c76 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js @@ -0,0 +1 @@ +/* nothing here */ diff --git a/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ new file mode 100644 index 0000000000..70592d2f93 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty-altsvc.js^headers^ @@ -0,0 +1 @@ +Alt-Svc: h2=":12345"; ma=60 diff --git a/toolkit/components/antitracking/test/browser/empty.html b/toolkit/components/antitracking/test/browser/empty.html new file mode 100644 index 0000000000..e20d67db57 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty.html @@ -0,0 +1 @@ +<h1>Empty</h1> diff --git a/toolkit/components/antitracking/test/browser/empty.js b/toolkit/components/antitracking/test/browser/empty.js new file mode 100644 index 0000000000..3053583c76 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/empty.js @@ -0,0 +1 @@ +/* nothing here */ diff --git a/toolkit/components/antitracking/test/browser/file_iframe_document_open.html b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html new file mode 100644 index 0000000000..fd2969f270 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_iframe_document_open.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script> +function run() { + let ifr = document.createElement("iframe"); + document.body.appendChild(ifr); + + let doc = ifr.contentWindow.document; + doc.open(); + doc.write(`<script>document.cookie = "foo=bar"<\/script>`); + doc.close(); +} +</script> +</head> +<body onLoad="run();"> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_localStorage.html b/toolkit/components/antitracking/test/browser/file_localStorage.html new file mode 100644 index 0000000000..54bad94bc9 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_localStorage.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1663192 - Accessing localStorage in a file urls</title> +</head> +<script> + window.addEventListener("DOMContentLoaded", () => { + let result = document.getElementById("result"); + + try { + window.localStorage.setItem("foo", "bar"); + result.textContent = "PASS"; + } catch (e) { + result.textContent = "FAIL"; + } + }, { once: true }); +</script> +<body> +<a id="result"></a> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs new file mode 100644 index 0000000000..2afb7d435f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsImage.sjs @@ -0,0 +1,20 @@ +// small red image +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html new file mode 100644 index 0000000000..aa3de2a555 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsPageInfo.html @@ -0,0 +1,6 @@ +<html> +<body> + <img src="http://example.net/browser/toolkit/components/antitracking/test/browser/raptor.jpg" id="image1"> + <video src="http://example.net/browser/toolkit/components/antitracking/test/browser/file_video.ogv" id="video1"> </video> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs new file mode 100644 index 0000000000..10bf246f62 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_saveAsVideo.sjs @@ -0,0 +1,38 @@ +const VIDEO = atob( + "GkXfo49CgoR3ZWJtQoeBAkKFgQIYU4BnI0nOEU2bdKpNu" + + "4tTq4QVSalmU6yBL027i1OrhBZUrmtTrIGmTbuLU6uEHF" + + "O7a1OsgdEVSalm8k2ApWxpYmVibWwyIHYwLjkuNyArIGx" + + "pYm1hdHJvc2thMiB2MC45LjhXQZ5ta2NsZWFuIDAuMi41" + + "IGZyb20gTGF2ZjUyLjU3LjFzpJC/CoyJjSbckmOytS9Se" + + "y3cRImIQK9AAAAAAABEYYgEHiZDUAHwABZUrmumrqTXgQ" + + "FzxYEBnIEAIrWcg3VuZIaFVl9WUDiDgQHgh7CCAUC6gfA" + + "cU7trzbuMs4EAt4f3gQHxggEju42zggMgt4f3gQHxgrZd" + + "u46zggZAt4j3gQHxgwFba7uOs4IJYLeI94EB8YMCAR+7j" + + "rOCDIC3iPeBAfGDAqggH0O2dSBibueBAKNbiYEAAIDQdw" + + "CdASpAAfAAAAcIhYWIhYSIAIIb347n/c/Z0BPBfjv7f+I" + + "/6df275Wbh/XPuZ+qv9k58KrftA9tvkP+efiN/ovmd/if" + + "9L/ef2z+Xv+H/rv+39xD/N/zL+nfid71363e9X+pf6/+q" + + "+x7+W/0P/H/233Zf6n/Wv696APuDf0b+YdcL+2HsUfxr+" + + "gfP/91P+F/yH9K+H79Z/7j/Xvhj/VjVsvwHXt/n/qz9U/" + + "w749+c/g=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Content-Type", "video/webm", false); + aResponse.setHeader( + "Cache-Control", + "public, max-age=604800, immutable", + false + ); + aResponse.write(VIDEO); + } +} diff --git a/toolkit/components/antitracking/test/browser/file_stripping.html b/toolkit/components/antitracking/test/browser/file_stripping.html new file mode 100644 index 0000000000..60d9840a0f --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_stripping.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf8"> +<script> + onmessage = event => { + switch (event.data.type) { + case "window-open": + window.open(event.data.url); + break; + case "script": + window.location.href = event.data.url; + break + } + }; +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/file_video.ogv b/toolkit/components/antitracking/test/browser/file_video.ogv Binary files differnew file mode 100644 index 0000000000..68dee3cf2b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_video.ogv diff --git a/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py new file mode 100644 index 0000000000..412ac2e24c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/file_ws_handshake_delay_wsh.py @@ -0,0 +1,28 @@ +import time + +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + # # must set request.ws_protocol to the selected version from ws_requested_protocols + for x in request.ws_requested_protocols: + if x != "test-does-not-exist": + request.ws_protocol = x + break + + if request.ws_protocol == "test-3": + time.sleep(3) + elif request.ws_protocol == "test-6": + time.sleep(6) + else: + pass + + +def web_socket_passive_closing_handshake(request): + if request.ws_close_code == 1005: + return None, None + return request.ws_close_code, request.ws_close_reason + + +def web_socket_transfer_data(request): + msgutil.close_connection(request) diff --git a/toolkit/components/antitracking/test/browser/head.js b/toolkit/components/antitracking/test/browser/head.js new file mode 100644 index 0000000000..da99cb433b --- /dev/null +++ b/toolkit/components/antitracking/test/browser/head.js @@ -0,0 +1,149 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_DOMAIN = "http://example.net/"; +const TEST_DOMAIN_HTTPS = "https://example.net/"; +const TEST_DOMAIN_2 = "http://xn--exmple-cua.test/"; +const TEST_DOMAIN_3 = "https://xn--hxajbheg2az3al.xn--jxalpdlp/"; +const TEST_DOMAIN_4 = "http://prefixexample.com/"; +const TEST_DOMAIN_5 = "http://test/"; +const TEST_DOMAIN_6 = "http://mochi.test:8888/"; +const TEST_DOMAIN_7 = "http://example.com/"; +const TEST_DOMAIN_8 = "http://www.example.com/"; +const TEST_3RD_PARTY_DOMAIN = "https://tracking.example.org/"; +const TEST_3RD_PARTY_DOMAIN_HTTP = "http://tracking.example.org/"; +const TEST_3RD_PARTY_DOMAIN_TP = "https://tracking.example.com/"; +const TEST_3RD_PARTY_DOMAIN_STP = "https://social-tracking.example.org/"; +const TEST_4TH_PARTY_DOMAIN = "http://not-tracking.example.com/"; +const TEST_4TH_PARTY_DOMAIN_HTTPS = "https://not-tracking.example.com/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP = + "http://another-tracking.example.net/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS = + "https://another-tracking.example.net/"; +const TEST_ANOTHER_3RD_PARTY_DOMAIN = SpecialPowers.useRemoteSubframes + ? TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTP + : TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS; +const TEST_EMAIL_TRACKER_DOMAIN = "http://email-tracking.example.org/"; + +const TEST_PATH = "browser/toolkit/components/antitracking/test/browser/"; + +const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_HTTPS = TEST_DOMAIN_HTTPS + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_2 = TEST_DOMAIN_2 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_3 = TEST_DOMAIN_3 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_4 = TEST_DOMAIN_4 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_5 = TEST_DOMAIN_5 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_6 = TEST_DOMAIN_6 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_7 = TEST_DOMAIN_7 + TEST_PATH + "page.html"; +const TEST_TOP_PAGE_8 = TEST_DOMAIN_8 + TEST_PATH + "page.html"; +const TEST_EMBEDDER_PAGE = TEST_DOMAIN + TEST_PATH + "embedder.html"; +const TEST_POPUP_PAGE = TEST_DOMAIN + TEST_PATH + "popup.html"; +const TEST_IFRAME_PAGE = TEST_DOMAIN + TEST_PATH + "iframe.html"; +const TEST_3RD_PARTY_PAGE = TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_PAGE_HTTP = + TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_PAGE_WO = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWO.html"; +const TEST_3RD_PARTY_PAGE_UI = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyUI.html"; +const TEST_3RD_PARTY_PAGE_WITH_SVG = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartySVG.html"; +const TEST_3RD_PARTY_PAGE_RELAY = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyRelay.html"; +const TEST_4TH_PARTY_PAGE = TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_3RD_PARTY_PAGE = + TEST_ANOTHER_3RD_PARTY_DOMAIN + TEST_PATH + "3rdParty.html"; +const TEST_ANOTHER_3RD_PARTY_PAGE_HTTPS = + TEST_ANOTHER_3RD_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdParty.html"; +const TEST_3RD_PARTY_STORAGE_PAGE = + TEST_3RD_PARTY_DOMAIN_HTTP + TEST_PATH + "3rdPartyStorage.html"; +const TEST_3RD_PARTY_PAGE_WORKER = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyWorker.html"; +const TEST_3RD_PARTY_PARTITIONED_PAGE = + TEST_3RD_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html"; +const TEST_4TH_PARTY_STORAGE_PAGE = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyStorage.html"; +const TEST_4TH_PARTY_STORAGE_PAGE_HTTPS = + TEST_4TH_PARTY_DOMAIN_HTTPS + TEST_PATH + "3rdPartyStorage.html"; +const TEST_4TH_PARTY_PARTITIONED_PAGE = + TEST_4TH_PARTY_DOMAIN + TEST_PATH + "3rdPartyPartitioned.html"; + +const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT; +const BEHAVIOR_REJECT = Ci.nsICookieService.BEHAVIOR_REJECT; +const BEHAVIOR_LIMIT_FOREIGN = Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN; +const BEHAVIOR_REJECT_FOREIGN = Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN; +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; +const BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN = + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + +let originalRequestLongerTimeout = requestLongerTimeout; +// eslint-disable-next-line no-global-assign +requestLongerTimeout = function AntiTrackingRequestLongerTimeout(factor) { + let ccovMultiplier = AppConstants.MOZ_CODE_COVERAGE ? 2 : 1; + let fissionMultiplier = SpecialPowers.useRemoteSubframes ? 2 : 1; + originalRequestLongerTimeout(ccovMultiplier * fissionMultiplier * factor); +}; + +requestLongerTimeout(3); + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/antitracking_head.js", + this +); + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/partitionedstorage_head.js", + this +); + +function setCookieBehaviorPref(cookieBehavior, runInPrivateWindow) { + let cbRegular; + let cbPrivate; + + // Set different cookieBehaviors to regular mode and private mode so that we + // can make sure these two prefs don't interfere with each other for all + // tests. + if (runInPrivateWindow) { + cbPrivate = cookieBehavior; + + let defaultPrefBranch = Services.prefs.getDefaultBranch(""); + // In order to test the default private cookieBehavior pref, we need to set + // the regular pref to the default value because we don't want the private + // pref to mirror the regular pref in this case. + // + // Note that the private pref will mirror the regular pref if the private + // pref is in default value and the regular pref is not in default value. + if ( + cookieBehavior == + defaultPrefBranch.getIntPref("network.cookie.cookieBehavior.pbmode") + ) { + cbRegular = defaultPrefBranch.getIntPref("network.cookie.cookieBehavior"); + } else { + cbRegular = + cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT; + } + } else { + cbRegular = cookieBehavior; + cbPrivate = + cookieBehavior == BEHAVIOR_ACCEPT ? BEHAVIOR_REJECT : BEHAVIOR_ACCEPT; + } + + return SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", cbRegular], + ["network.cookie.cookieBehavior.pbmode", cbPrivate], + ], + }); +} diff --git a/toolkit/components/antitracking/test/browser/iframe.html b/toolkit/components/antitracking/test/browser/iframe.html new file mode 100644 index 0000000000..85d37ed7fa --- /dev/null +++ b/toolkit/components/antitracking/test/browser/iframe.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Just a first-level iframe</title> +</head> +<body> + <h1>This is the first-level iframe</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/image.sjs b/toolkit/components/antitracking/test/browser/image.sjs new file mode 100644 index 0000000000..145ff1f0a4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/image.sjs @@ -0,0 +1,22 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState("hints") || 0); + setState("hints", "0"); + } else { + let hints = parseInt(getState("hints") || 0) + 1; + setState("hints", hints.toString()); + + aResponse.setHeader("Set-Cookie", "foopy=1"); + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/imageCacheWorker.js b/toolkit/components/antitracking/test/browser/imageCacheWorker.js new file mode 100644 index 0000000000..d11221112c --- /dev/null +++ b/toolkit/components/antitracking/test/browser/imageCacheWorker.js @@ -0,0 +1,78 @@ +/* import-globals-from head.js */ +/* import-globals-from antitracking_head.js */ +/* import-globals-from browser_imageCache4.js */ + +AntiTracking.runTest( + "Image cache - should load the image three times.", + // blocking callback + async _ => { + // Let's load the image twice here. + let img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 1 loaded"); + + img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 2 loaded"); + }, + + // non-blocking callback + { + runExtraTests: false, + cookieBehavior, + blockingByAllowList, + expectedBlockingNotifications, + callback: async _ => { + // Let's load the image twice here as well. + let img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 3 loaded"); + + img = document.createElement("img"); + document.body.appendChild(img); + img.src = + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs"; + await new Promise(resolve => { + img.onload = resolve; + }); + ok(true, "Image 4 loaded"); + }, + }, + null, // cleanup function + null, // no extra prefs + false, // no window open test + false, // no user-interaction test + expectedBlockingNotifications +); + +// We still want to see just expected requests. +add_task(async _ => { + await fetch( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/image.sjs?result" + ) + .then(r => r.text()) + .then(text => { + is(text, "2", "The image should be loaded correctly."); + }); + + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); +}); diff --git a/toolkit/components/antitracking/test/browser/localStorage.html b/toolkit/components/antitracking/test/browser/localStorage.html new file mode 100644 index 0000000000..e08c25f2c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/localStorage.html @@ -0,0 +1,68 @@ +<h1>Here a tracker!</h1> +<script> + +if (window.opener) { + SpecialPowers.wrap(document).userInteractionForTesting(); + localStorage.foo = "opener" + Math.random(); + // Don't call window.close immediatelly. It can happen that adding the + // "storage" event listener below takes more time than usual (it may need to + // synchronously subscribe in the parent process to receive storage + // notifications). Spending more time in the initial script can prevent + // the "load" event from being fired for the window opened by "open and test". + setTimeout(() => { + window.close(); + }, 0); +} + +if (parent) { + window.onmessage = e => { + if (e.data == "test") { + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status }, "*"); + return; + } + + if (e.data == "open") { + window.open("localStorage.html"); + return; + } + + if (e.data == "open and test") { + let w = window.open("localStorage.html"); + w.addEventListener("load", _ => { + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status }, "*"); + }, {once: true}); + } + }; + + window.addEventListener("storage", e => { + let fromOpener = localStorage.foo.startsWith("opener"); + + let status; + try { + localStorage.foo = "value" + Math.random(); + status = true; + } catch (e) { + status = false; + } + + parent.postMessage({type: "test", status: status && fromOpener }, "*"); + }); +} + +</script> diff --git a/toolkit/components/antitracking/test/browser/localStorageEvents.html b/toolkit/components/antitracking/test/browser/localStorageEvents.html new file mode 100644 index 0000000000..737d1e0cab --- /dev/null +++ b/toolkit/components/antitracking/test/browser/localStorageEvents.html @@ -0,0 +1,30 @@ +<script> + +let eventCounter = 0; + +onmessage = e => { + if (e.data == "getValue") { + parent.postMessage(localStorage.foo, "*"); + return; + } + + if (e.data == "setValue") { + localStorage.foo = "tracker-" + Math.random(); + return; + } + + if (e.data == "getEvents") { + parent.postMessage(eventCounter, "*"); + return; + } + + if (e.data == "reload") { + window.location.reload(); + } +}; + +addEventListener("storage", _ => { + ++eventCounter; +}); + +</script> diff --git a/toolkit/components/antitracking/test/browser/matchAll.js b/toolkit/components/antitracking/test/browser/matchAll.js new file mode 100644 index 0000000000..8f112b0804 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/matchAll.js @@ -0,0 +1,16 @@ +self.addEventListener("message", async e => { + let clients = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + let hasWindow = false; + for (let client of clients) { + if (e.data == client.url) { + hasWindow = true; + break; + } + } + + e.source.postMessage(hasWindow); +}); diff --git a/toolkit/components/antitracking/test/browser/page.html b/toolkit/components/antitracking/test/browser/page.html new file mode 100644 index 0000000000..a99e8be179 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/page.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Just a top-level page</title> +</head> +<body> + <h1>This is the top-level page</h1> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js new file mode 100644 index 0000000000..5ac4ec9f27 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/partitionedSharedWorker.js @@ -0,0 +1,17 @@ +let value = ""; +self.onconnect = e => { + e.ports[0].onmessage = event => { + if (event.data.what === "get") { + e.ports[0].postMessage(value); + return; + } + + if (event.data.what === "put") { + value = event.data.value; + return; + } + + // Error. + e.ports[0].postMessage(-1); + }; +}; diff --git a/toolkit/components/antitracking/test/browser/partitionedstorage_head.js b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js new file mode 100644 index 0000000000..42574fdb93 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/partitionedstorage_head.js @@ -0,0 +1,455 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from head.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/antitracking/test/browser/dynamicfpi_head.js", + this +); + +this.PartitionedStorageHelper = { + runTestInNormalAndPrivateMode(name, callback, cleanupFunction, extraPrefs) { + // Normal mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInPrivateWindow: false, + }); + + // Private mode + this.runTest(name, callback, cleanupFunction, extraPrefs, { + runInPrivateWindow: true, + }); + }, + + runTest( + name, + callback, + cleanupFunction, + extraPrefs, + { runInPrivateWindow = false, runInSecureContext = false } = {} + ) { + DynamicFPIHelper.runTest( + name, + callback, + cleanupFunction, + extraPrefs, + runInPrivateWindow, + { runInSecureContext } + ); + }, + + runPartitioningTestInNormalAndPrivateMode( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition = false + ) { + // Normal mode + this.runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + false + ); + + // Private mode + this.runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + true + ); + }, + + runPartitioningTest( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + expectUnpartition, + runInPrivateWindow = false + ) { + for (let variant of ["normal", "initial-aboutblank"]) { + for (let limitForeignContexts of [false, true]) { + this.runPartitioningTestInner( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + variant, + runInPrivateWindow, + limitForeignContexts, + expectUnpartition + ); + } + } + }, + + runPartitioningTestInner( + name, + testCategory, + getDataCallback, + addDataCallback, + cleanupFunction, + variant, + runInPrivateWindow, + limitForeignContexts, + expectUnpartition + ) { + add_task(async _ => { + info( + "Starting test `" + + name + + "' testCategory `" + + testCategory + + "' variant `" + + variant + + "' in a " + + (runInPrivateWindow ? "private" : "normal") + + " window " + + (limitForeignContexts ? "with" : "without") + + " limitForeignContexts to check that 2 tabs are correctly partititioned" + ); + + await SpecialPowers.flushPrefEnv(); + await setCookieBehaviorPref( + BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + runInPrivateWindow + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.enabled", true], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + true, + ], + ["privacy.dynamic_firstparty.limitForeign", limitForeignContexts], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["dom.security.https_first_pbm", false], + [ + "privacy.restrict3rdpartystorage.userInteractionRequiredForHosts", + "not-tracking.example.com", + ], + ], + }); + + let win = window; + if (runInPrivateWindow) { + win = OpenBrowserWindow({ private: true }); + await TestUtils.topicObserved("browser-delayed-startup-finished"); + } + + info("Creating the first tab"); + let tab1 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab1; + + let browser1 = win.gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser1); + + info("Creating the second tab"); + let tab2 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE_6); + win.gBrowser.selectedTab = tab2; + + let browser2 = win.gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + info("Creating the third tab"); + let tab3 = BrowserTestUtils.addTab( + win.gBrowser, + TEST_4TH_PARTY_PARTITIONED_PAGE + ); + win.gBrowser.selectedTab = tab3; + + let browser3 = win.gBrowser.getBrowserForTab(tab3); + await BrowserTestUtils.browserLoaded(browser3); + + // Use the same URL as first tab to check partitioned data + info("Creating the forth tab"); + let tab4 = BrowserTestUtils.addTab(win.gBrowser, TEST_TOP_PAGE); + win.gBrowser.selectedTab = tab4; + + let browser4 = win.gBrowser.getBrowserForTab(tab4); + await BrowserTestUtils.browserLoaded(browser4); + + async function getDataFromThirdParty(browser, result) { + // Overwrite the special case here since third party cookies are not + // avilable when `limitForeignContexts` is enabled. + if (testCategory === "cookies" && limitForeignContexts) { + info("overwrite result to empty"); + result = ""; + } + + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant, + getDataCallback: getDataCallback.toString(), + result, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = __ => { + info("Sending code to the 3rd party content"); + ifr.contentWindow.postMessage({ cb: obj.getDataCallback }, "*"); + }; + + content.addEventListener( + "message", + function msg(event) { + is( + event.data, + obj.result, + "Partitioned cookie jar has value: " + obj.result + ); + resolve(); + }, + { once: true } + ); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + } + + async function getDataFromFirstParty(browser, result) { + await SpecialPowers.spawn( + browser, + [ + { + getDataCallback: getDataCallback.toString(), + result, + variant, + }, + ], + async obj => { + let runnableStr = `(() => {return (${obj.getDataCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let win = content; + if (obj.variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(content, win); + is( + result, + obj.result, + "Partitioned cookie jar is empty: " + obj.result + ); + } + ); + } + + info("Checking 3rd party has an empty cookie jar in first tab"); + await getDataFromThirdParty(browser1, ""); + + info("Checking 3rd party has an empty cookie jar in second tab"); + await getDataFromThirdParty(browser2, ""); + + info("Checking first party has an empty cookie jar in third tab"); + await getDataFromFirstParty(browser3, ""); + + info("Checking 3rd party has an empty cookie jar in forth tab"); + await getDataFromThirdParty(browser4, ""); + + async function createDataInThirdParty(browser, value) { + await SpecialPowers.spawn( + browser, + [ + { + page: TEST_4TH_PARTY_PARTITIONED_PAGE + "?variant=" + variant, + addDataCallback: addDataCallback.toString(), + value, + }, + ], + async obj => { + await new content.Promise(resolve => { + let ifr = content.document.getElementsByTagName("iframe")[0]; + content.addEventListener( + "message", + function msg(event) { + ok(event.data, "Data created"); + resolve(); + }, + { once: true } + ); + + ifr.contentWindow.postMessage( + { + cb: obj.addDataCallback, + value: obj.value, + }, + "*" + ); + }); + } + ); + } + + async function createDataInFirstParty(browser, value) { + await SpecialPowers.spawn( + browser, + [ + { + addDataCallback: addDataCallback.toString(), + value, + variant, + }, + ], + async obj => { + let runnableStr = `(() => {return (${obj.addDataCallback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + let win = content; + if (obj.variant == "initial-aboutblank") { + let i = win.document.createElement("iframe"); + i.src = "about:blank"; + win.document.body.appendChild(i); + // override win to make it point to the initial about:blank window + win = i.contentWindow; + } + + let result = await runnable.call(content, win, obj.value); + ok(result, "Data created"); + } + ); + } + + info("Creating data in the first tab"); + await createDataInThirdParty(browser1, "A"); + + info("Creating data in the second tab"); + await createDataInThirdParty(browser2, "B"); + + // Before writing browser4, check data written by browser1 + info("First tab should still have just 'A'"); + await getDataFromThirdParty(browser1, "A"); + info("Forth tab should still have just 'A'"); + await getDataFromThirdParty(browser4, "A"); + + // Ensure to create data in the forth tab before the third tab, + // otherwise cookie will be written successfully due to prior cookie + // of the base domain exists. + info("Creating data in the forth tab"); + await createDataInThirdParty(browser4, "D"); + + info("Creating data in the third tab"); + await createDataInFirstParty(browser3, "C"); + + // read all tabs + info("First tab should be changed to 'D'"); + await getDataFromThirdParty(browser1, "D"); + + info("Second tab should still have just 'B'"); + await getDataFromThirdParty(browser2, "B"); + + info("Third tab should still have just 'C'"); + await getDataFromFirstParty(browser3, "C"); + + info("Forth tab should still have just 'D'"); + await getDataFromThirdParty(browser4, "D"); + + async function setStorageAccessForThirdParty(browser) { + info(`Setting permission for ${browser.currentURI.spec}`); + let type = "3rdPartyStorage^http://not-tracking.example.com"; + let permission = Services.perms.ALLOW_ACTION; + let expireType = Services.perms.EXPIRE_SESSION; + Services.perms.addFromPrincipal( + browser.contentPrincipal, + type, + permission, + expireType, + 0 + ); + // Wait for permission to be set successfully + let originAttributes = runInPrivateWindow + ? { privateBrowsingId: 1 } + : {}; + await new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission(type, permission, { + url: browser.currentURI.spec, + originAttributes, + }) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); + } + + if (!expectUnpartition) { + info("Setting Storage access for third parties"); + + await setStorageAccessForThirdParty(browser1); + await setStorageAccessForThirdParty(browser2); + await setStorageAccessForThirdParty(browser3); + await setStorageAccessForThirdParty(browser4); + + info("Done setting Storage access for third parties"); + + // read all tabs + info("First tab should still have just 'D'"); + await getDataFromThirdParty(browser1, "D"); + + info("Second tab should still have just 'B'"); + await getDataFromThirdParty(browser2, "B"); + + info("Third tab should still have just 'C'"); + await getDataFromFirstParty(browser3, "C"); + + info("Forth tab should still have just 'D'"); + await getDataFromThirdParty(browser4, "D"); + } + + info("Done checking departitioned state"); + + info("Removing the tabs"); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + BrowserTestUtils.removeTab(tab3); + BrowserTestUtils.removeTab(tab4); + + if (runInPrivateWindow) { + win.close(); + } + }); + + add_task(async _ => { + info("Cleaning up."); + if (cleanupFunction) { + await cleanupFunction(); + } + + // While running these tests we typically do not have enough idle time to do + // GC reliably, so force it here. + /* import-globals-from antitracking_head.js */ + forceGC(); + }); + }, +}; diff --git a/toolkit/components/antitracking/test/browser/popup.html b/toolkit/components/antitracking/test/browser/popup.html new file mode 100644 index 0000000000..f195add788 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/popup.html @@ -0,0 +1,11 @@ +<html> +<head> + <title>Just a popup that does a redirect</title> +</head> +<body> + <h1>Just a popup that does a redirect</h1> + <script> + window.location = "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html"; + </script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/browser/raptor.jpg b/toolkit/components/antitracking/test/browser/raptor.jpg Binary files differnew file mode 100644 index 0000000000..243ba9e2d4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/raptor.jpg diff --git a/toolkit/components/antitracking/test/browser/redirect.sjs b/toolkit/components/antitracking/test/browser/redirect.sjs new file mode 100644 index 0000000000..4645aedca5 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/redirect.sjs @@ -0,0 +1,11 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 302); + + let query = aRequest.queryString; + let locations = query.split("|"); + let nextLocation = locations.shift(); + if (locations.length) { + nextLocation += "?" + locations.join("|"); + } + aResponse.setHeader("Location", nextLocation); +} diff --git a/toolkit/components/antitracking/test/browser/referrer.sjs b/toolkit/components/antitracking/test/browser/referrer.sjs new file mode 100644 index 0000000000..8e80eb7fa3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/referrer.sjs @@ -0,0 +1,49 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +const IFRAME = + "<!DOCTYPE html>\n" + + "<script>\n" + + "onmessage = event => {\n" + + "parent.postMessage(document.referrer, '*');\n" + + "};\n" + + "</script>"; + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + let key; + if (aRequest.queryString.includes("what=script")) { + key = "script"; + } else if (aRequest.queryString.includes("what=image")) { + key = "image"; + } else { + key = "iframe"; + } + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState(key)); + setState(key, ""); + return; + } + + if (aRequest.hasHeader("Referer")) { + let referrer = aRequest.getHeader("Referer"); + setState(key, referrer); + } + + if (key == "script") { + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42;"); + } else if (key == "image") { + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } else { + aResponse.setHeader("Content-Type", "text/html", false); + aResponse.write(IFRAME); + } +} diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html b/toolkit/components/antitracking/test/browser/sandboxed.html new file mode 100644 index 0000000000..a359ae0aa3 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sandboxed.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html dir="ltr" xml:lang="en-US" lang="en-US"> + <head> + <meta charset="utf8"> + </head> + <body> + <p>Hello, World!</p> + </body> +</html> diff --git a/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ new file mode 100644 index 0000000000..4705ce9ded --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sandboxed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: sandbox allow-scripts; diff --git a/toolkit/components/antitracking/test/browser/server.sjs b/toolkit/components/antitracking/test/browser/server.sjs new file mode 100644 index 0000000000..4d1b2ef01a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/server.sjs @@ -0,0 +1,20 @@ +function handleRequest(aRequest, aResponse) { + if (aRequest.queryString.includes("redirect")) { + aResponse.setStatusLine(aRequest.httpVersion, 302); + if (aRequest.queryString.includes("redirect-checkonly")) { + aResponse.setHeader("Location", "server.sjs?checkonly"); + } else { + aResponse.setHeader("Location", "server.sjs"); + } + return; + } + aResponse.setStatusLine(aRequest.httpVersion, 200); + if (aRequest.hasHeader("Cookie")) { + aResponse.write("cookie-present"); + } else { + if (!aRequest.queryString.includes("checkonly")) { + aResponse.setHeader("Set-Cookie", "foopy=1"); + } + aResponse.write("cookie-not-present"); + } +} diff --git a/toolkit/components/antitracking/test/browser/serviceWorker.js b/toolkit/components/antitracking/test/browser/serviceWorker.js new file mode 100644 index 0000000000..71c88c721a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/serviceWorker.js @@ -0,0 +1,103 @@ +let value = ""; +let fetch_url = ""; + +self.onfetch = function (e) { + fetch_url = e.request.url; +}; + +// Call clients.claim() to enable fetch event. +self.addEventListener("activate", e => { + e.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", async e => { + let res = {}; + + switch (e.data.type) { + case "GetHWConcurrency": + res.result = "OK"; + res.value = navigator.hardwareConcurrency; + break; + + case "GetScriptValue": + res.result = "OK"; + res.value = value; + break; + + case "SetScriptValue": + res.result = "OK"; + value = e.data.value; + break; + + case "HasCache": + // Return if the cache storage exists or not. + try { + res.value = await caches.has(e.data.value); + res.result = "OK"; + } catch (e) { + res.result = "ERROR"; + } + break; + + case "SetCache": + // Open a cache storage with the given name. + try { + let cache = await caches.open(e.data.value); + await cache.add("empty.js"); + res.result = "OK"; + } catch (e) { + res.result = "ERROR"; + } + break; + + case "SetIndexedDB": + await new Promise(resolve => { + let idxDB = indexedDB.open("test", 1); + + idxDB.onupgradeneeded = evt => { + let db = evt.target.result; + db.createObjectStore("foobar", { keyPath: "id" }); + }; + + idxDB.onsuccess = evt => { + let db = evt.target.result; + db + .transaction("foobar", "readwrite") + .objectStore("foobar") + .put({ id: 1, value: e.data.value }).onsuccess = _ => { + resolve(); + }; + }; + }); + res.result = "OK"; + break; + + case "GetIndexedDB": + res.value = await new Promise(resolve => { + let idxDB = indexedDB.open("test", 1); + + idxDB.onsuccess = evt => { + let db = evt.target.result; + db.transaction("foobar").objectStore("foobar").get(1).onsuccess = + ee => { + resolve( + ee.target.result === undefined ? "" : ee.target.result.value + ); + }; + }; + }); + res.result = "OK"; + break; + + case "GetFetchURL": + res.value = fetch_url; + fetch_url = ""; + res.result = "OK"; + break; + + default: + res.result = "ERROR"; + } + + e.source.postMessage(res); +}); diff --git a/toolkit/components/antitracking/test/browser/sharedWorker.js b/toolkit/components/antitracking/test/browser/sharedWorker.js new file mode 100644 index 0000000000..01188ed10a --- /dev/null +++ b/toolkit/components/antitracking/test/browser/sharedWorker.js @@ -0,0 +1,18 @@ +let ports = 0; +self.onconnect = e => { + ++ports; + e.ports[0].onmessage = event => { + if (event.data === "count") { + e.ports[0].postMessage(ports); + return; + } + + if (event.data === "close") { + self.close(); + return; + } + + // Error. + e.ports[0].postMessage(-1); + }; +}; diff --git a/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js new file mode 100644 index 0000000000..6a983ec250 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js @@ -0,0 +1,236 @@ +/* global allowListed */ + +async function hasStorageAccessInitially() { + let hasAccess = await document.hasStorageAccess(); + ok(hasAccess, "Has storage access"); +} + +async function noStorageAccessInitially() { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Doesn't yet have storage access"); +} + +async function stillNoStorageAccess() { + let hasAccess = await document.hasStorageAccess(); + ok(!hasAccess, "Still doesn't have storage access"); +} + +async function callRequestStorageAccess(callback, expectFail) { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + + let origin = new URL(location.href).origin; + + let effectiveCookieBehavior = SpecialPowers.isContentWindowPrivate(window) + ? SpecialPowers.Services.prefs.getIntPref( + "network.cookie.cookieBehavior.pbmode" + ) + : SpecialPowers.Services.prefs.getIntPref("network.cookie.cookieBehavior"); + + let success = true; + // We only grant storage exceptions when the reject tracker behavior is enabled. + let rejectTrackers = + [ + SpecialPowers.Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + SpecialPowers.Ci.nsICookieService + .BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ].includes(effectiveCookieBehavior) && !isOnContentBlockingAllowList(); + const TEST_ANOTHER_3RD_PARTY_ORIGIN = SpecialPowers.useRemoteSubframes + ? "http://another-tracking.example.net" + : "https://another-tracking.example.net"; + // With another-tracking.example.net, we're same-eTLD+1, so the first try succeeds. + if (origin != TEST_ANOTHER_3RD_PARTY_ORIGIN) { + if (rejectTrackers) { + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } + ok(!threw, "requestStorageAccess should not throw"); + try { + if (callback) { + if (expectFail) { + await p.catch(_ => callback()); + success = false; + } else { + await p.then(_ => callback()); + } + } else { + await p; + } + } catch (e) { + success = false; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + ok(!success, "Should not have worked without user interaction"); + + await noStorageAccessInitially(); + + await interactWithTracker(); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } + if ( + effectiveCookieBehavior == + SpecialPowers.Ci.nsICookieService.BEHAVIOR_ACCEPT && + !isOnContentBlockingAllowList() + ) { + try { + if (callback) { + if (expectFail) { + await document.requestStorageAccess().catch(_ => callback()); + success = false; + } else { + await document.requestStorageAccess().then(_ => callback()); + } + } else { + await document.requestStorageAccess(); + } + } catch (e) { + success = false; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + ok(success, "Should not have thrown"); + + await hasStorageAccessInitially(); + + await interactWithTracker(); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + } + } + + let p; + let threw = false; + try { + p = document.requestStorageAccess(); + } catch (e) { + threw = true; + } + let rejected = false; + try { + if (callback) { + if (expectFail) { + await p.catch(_ => callback()); + rejected = true; + } else { + await p.then(_ => callback()); + } + } else { + await p; + } + } catch (e) { + rejected = true; + } finally { + SpecialPowers.wrap(document).clearUserGestureActivation(); + } + + success = !threw && !rejected; + let hasAccess = await document.hasStorageAccess(); + is( + hasAccess, + success, + "Should " + (success ? "" : "not ") + "have storage access now" + ); + if ( + success && + rejectTrackers && + window.location.search != "?disableWaitUntilPermission" && + origin != TEST_ANOTHER_3RD_PARTY_ORIGIN + ) { + let protocol = isSecureContext ? "https" : "http"; + // Wait until the permission is visible in parent process to avoid race + // conditions. We don't need to wait the permission to be visible in content + // processes since the content process doesn't rely on the permission to + // know the storage access is updated. + await waitUntilPermission( + `${protocol}://example.net/browser/toolkit/components/antitracking/test/browser/page.html`, + "3rdPartyStorage^" + window.origin + ); + } + + return [threw, rejected]; +} + +async function waitUntilPermission( + url, + name, + value = SpecialPowers.Services.perms.ALLOW_ACTION +) { + let originAttributes = SpecialPowers.isContentWindowPrivate(window) + ? { privateBrowsingId: 1 } + : {}; + await new Promise(resolve => { + let id = setInterval(async _ => { + if ( + await SpecialPowers.testPermission(name, value, { + url, + originAttributes, + }) + ) { + clearInterval(id); + resolve(); + } + }, 0); + }); +} + +async function interactWithTracker() { + await new Promise(resolve => { + let orionmessage = onmessage; + onmessage = _ => { + onmessage = orionmessage; + resolve(); + }; + + info("Let's interact with the tracker"); + window.open( + "/browser/toolkit/components/antitracking/test/browser/3rdPartyOpenUI.html?messageme" + ); + }); + + // Wait until the user interaction permission becomes visible in our process + await waitUntilPermission(window.origin, "storageAccessAPI"); +} + +function isOnContentBlockingAllowList() { + // We directly check the window.allowListed here instead of checking the + // permission. The allow list permission might not be available since it is + // not in the preload list. + + return window.allowListed; +} + +async function registerServiceWorker(win, url) { + let reg = await win.navigator.serviceWorker.register(url); + if (reg.installing.state !== "activated") { + await new Promise(resolve => { + let w = reg.installing; + w.addEventListener("statechange", function onStateChange() { + if (w.state === "activated") { + w.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + + return reg.active; +} + +function sendAndWaitWorkerMessage(target, worker, message) { + return new Promise(resolve => { + worker.addEventListener( + "message", + msg => { + resolve(msg.data); + }, + { once: true } + ); + + target.postMessage(message); + }); +} diff --git a/toolkit/components/antitracking/test/browser/storage_access_head.js b/toolkit/components/antitracking/test/browser/storage_access_head.js new file mode 100644 index 0000000000..274e128783 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/storage_access_head.js @@ -0,0 +1,248 @@ +/* import-globals-from ../../../../../browser/modules/test/browser/head.js */ +/* import-globals-from antitracking_head.js */ + +async function openPageAndRunCode( + topPage, + topPageCallback, + embeddedPage, + embeddedPageCallback +) { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: topPage, + waitForLoad: true, + }); + let browser = gBrowser.getBrowserForTab(tab); + + await topPageCallback(); + await SpecialPowers.spawn( + browser, + [{ page: embeddedPage, callback: embeddedPageCallback.toString() }], + async function (obj) { + await new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.onload = function () { + ifr.contentWindow.postMessage(obj.callback, "*"); + }; + + content.addEventListener("message", function msg(event) { + if (event.data.type == "finish") { + content.removeEventListener("message", msg); + resolve(); + return; + } + + if (event.data.type == "ok") { + ok(event.data.what, event.data.msg); + return; + } + + if (event.data.type == "info") { + info(event.data.msg); + return; + } + + ok(false, "Unknown message"); + }); + + content.document.body.appendChild(ifr); + ifr.src = obj.page; + }); + } + ); + + await BrowserTestUtils.removeTab(tab); +} + +// This function returns a function that spawns an asynchronous task to handle +// the popup and click on the appropriate values. If that task is never executed +// the catch case is reached and we fail the test. If for some reason that catch +// case isn't reached, having an extra event listener at the end of the test +// will cause the test to fail anyway. +// Note: this means that tests that use this callback should probably be in +// their own test file. +function getExpectPopupAndClick(accept) { + return function () { + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise + .then(async _ => { + // This occurs when the promise resolves on the test finishing + let popupNotifications = PopupNotifications.panel.childNodes; + if (!popupNotifications.length) { + ok(false, "Prompt did not show up"); + } else if (accept == "accept") { + ok(true, "Prompt shows up, clicking accept."); + await clickMainAction(); + } else if (accept == "reject") { + ok(true, "Prompt shows up, clicking reject."); + await clickSecondaryAction(); + } else { + ok(false, "Unknown accept value for test: " + accept); + info("Clicking accept so that the test can finish."); + await clickMainAction(); + } + }) + .catch(() => { + ok(false, "Prompt did not show up"); + }); + }; +} + +// This function spawns an asynchronous task that fails the test if a popup +// appears. If that never happens, the catch case is executed on the test +// cleanup. +// Note: this means that tests that use this callback should probably be in +// their own test file. +function expectNoPopup() { + let shownPromise = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + shownPromise + .then(async _ => { + // This occurs when the promise resolves on the test finishing + let popupNotifications = PopupNotifications.panel.childNodes; + if (!popupNotifications.length) { + ok(true, "Prompt did not show up"); + } else { + ok(false, "Prompt shows up"); + info(PopupNotifications.panel); + await clickSecondaryAction(); + } + }) + .catch(() => { + ok(true, "Prompt did not show up"); + }); +} + +async function requestStorageAccessAndExpectSuccess() { + const aps = SpecialPowers.Services.prefs.getBoolPref( + "privacy.partition.always_partition_third_party_non_cookie_storage" + ); + + // When always partitioning storage, we do not clear non-cookie storage + // after a requestStorageAccess is accepted by the user. So here we test + // that indexedDB is cleared when the pref is off, but not when it is on. + await new Promise((resolve, reject) => { + const db = window.indexedDB.open("rSATest", 1); + db.onupgradeneeded = resolve; + db.success = resolve; + db.onerror = reject; + }); + + const hadAccessAlready = await document.hasStorageAccess(); + const shouldClearIDB = !aps && !hadAccessAlready; + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(true, "gain storage access."); + } catch { + ok(false, "denied storage access."); + } + + await new Promise((resolve, reject) => { + const req = window.indexedDB.open("rSATest", 1); + req.onerror = reject; + req.onupgradeneeded = () => { + ok(shouldClearIDB, "iDB was cleared"); + req.onsuccess = undefined; + resolve(); + }; + req.onsuccess = () => { + ok(!shouldClearIDB, "iDB was not cleared"); + resolve(); + }; + }); + + await new Promise(resolve => { + const req = window.indexedDB.deleteDatabase("rSATest"); + req.onsuccess = resolve; + req.onerror = resolve; + }); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +async function requestStorageAccessAndExpectFailure() { + // When always partitioning storage, we do not clear non-cookie storage + // after a requestStorageAccess is accepted by the user. So here we test + // that indexedDB is cleared when the pref is off, but not when it is on. + await new Promise((resolve, reject) => { + const db = window.indexedDB.open("rSATest", 1); + db.onupgradeneeded = resolve; + db.success = resolve; + db.onerror = reject; + }); + + SpecialPowers.wrap(document).notifyUserGestureActivation(); + let p = document.requestStorageAccess(); + try { + await p; + ok(false, "gain storage access."); + } catch { + ok(true, "denied storage access."); + } + + await new Promise((resolve, reject) => { + const req = window.indexedDB.open("rSATest", 1); + req.onerror = reject; + req.onupgradeneeded = () => { + ok(false, "iDB was cleared"); + req.onsuccess = undefined; + resolve(); + }; + req.onsuccess = () => { + ok(true, "iDB was not cleared"); + resolve(); + }; + }); + + await new Promise(resolve => { + const req = window.indexedDB.deleteDatabase("rSATest"); + req.onsuccess = resolve; + req.onerror = resolve; + }); + + SpecialPowers.wrap(document).clearUserGestureActivation(); +} + +async function cleanUpData() { + await new Promise(resolve => { + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => + resolve() + ); + }); + ok(true, "Deleted all data."); +} + +async function setPreferences(alwaysPartitionStorage = false) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.storage_access.auto_grants", true], + ["dom.storage_access.auto_grants.delayed", false], + ["dom.storage_access.enabled", true], + ["dom.storage_access.max_concurrent_auto_grants", 0], + ["dom.storage_access.prompt.testing", false], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "network.cookie.cookieBehavior.pbmode", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + [ + "privacy.partition.always_partition_third_party_non_cookie_storage", + alwaysPartitionStorage, + ], + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ], + }); +} diff --git a/toolkit/components/antitracking/test/browser/subResources.sjs b/toolkit/components/antitracking/test/browser/subResources.sjs new file mode 100644 index 0000000000..f40d5cac97 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/subResources.sjs @@ -0,0 +1,33 @@ +// A 1x1 PNG image. +// Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) +const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" +); + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + let key = aRequest.queryString.includes("what=script") ? "script" : "image"; + + if (aRequest.queryString.includes("result")) { + aResponse.write(getState(key) || 0); + setState(key, "0"); + return; + } + + if (aRequest.hasHeader("Cookie")) { + let hints = parseInt(getState(key) || 0) + 1; + setState(key, hints.toString()); + } + + aResponse.setHeader("Set-Cookie", "foopy=1"); + + if (key == "script") { + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42;"); + } else { + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + } +} diff --git a/toolkit/components/antitracking/test/browser/tracker.js b/toolkit/components/antitracking/test/browser/tracker.js new file mode 100644 index 0000000000..85e943f7c4 --- /dev/null +++ b/toolkit/components/antitracking/test/browser/tracker.js @@ -0,0 +1,7 @@ +window.addEventListener("message", e => { + let bc = new BroadcastChannel("a"); + bc.postMessage("ready!"); +}); +window.open( + "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/3rdPartyOpen.html" +); diff --git a/toolkit/components/antitracking/test/browser/workerIframe.html b/toolkit/components/antitracking/test/browser/workerIframe.html new file mode 100644 index 0000000000..37aa5d7c0d --- /dev/null +++ b/toolkit/components/antitracking/test/browser/workerIframe.html @@ -0,0 +1,71 @@ +<html> +<head> + <title>3rd party content!</title> + <script type="text/javascript" src="https://example.com/browser/toolkit/components/antitracking/test/browser/storageAccessAPIHelpers.js"></script> +</head> +<body> +<h1>Here the 3rd party content!</h1> +<script> + +function info(msg) { + parent.postMessage({ type: "info", msg }, "*"); +} + +function ok(what, msg) { + parent.postMessage({ type: "ok", what: !!what, msg }, "*"); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +async function runTest() { + function workerCode() { + onmessage = e => { + try { + indexedDB.open("test", "1"); + postMessage(true); + } catch (e) { + postMessage(false); + } + }; + } + + /* import-globals-from storageAccessAPIHelpers.js */ + await noStorageAccessInitially(); + info("Initialized"); + + let blob = new Blob([workerCode.toString() + "; workerCode();"]); + let blobURL = URL.createObjectURL(blob); + info("Blob created"); + + let w = new Worker(blobURL); + info("Worker created"); + + await new Promise(resolve => { + w.addEventListener("message", e => { + ok(!e.data, "IDB is disabled"); + resolve(); + }, { once: true }); + w.postMessage("go"); + }); + + /* import-globals-from storageAccessAPIHelpers.js */ + await callRequestStorageAccess(); + + await new Promise(resolve => { + w.addEventListener("message", e => { + ok(e.data, "IDB is enabled"); + resolve(); + }, { once: true }); + w.postMessage("go"); + }); + + parent.postMessage({ type: "finish" }, "*"); +} + +runTest(); + +</script> +</body> +</html> diff --git a/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp new file mode 100644 index 0000000000..1aa0614622 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestPartitioningExceptionList.cpp @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/Preferences.h" +#include "mozilla/PartitioningExceptionList.h" + +using namespace mozilla; + +static const char kPrefPartitioningExceptionList[] = + "privacy.restrict3rdpartystorage.skip_list"; + +static const char kPrefEnableWebcompat[] = + "privacy.antitracking.enableWebcompat"; + +TEST(TestPartitioningExceptionList, TestPrefBasic) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + for (uint32_t populateList = 0; populateList <= 1; populateList++) { + for (uint32_t enableWebcompat = 0; enableWebcompat <= 1; + enableWebcompat++) { + if (populateList) { + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com,https://example.net"); + } else { + Preferences::SetCString(kPrefPartitioningExceptionList, ""); + } + + Preferences::SetBool(kPrefEnableWebcompat, enableWebcompat); + + EXPECT_FALSE( + PartitioningExceptionList::Check(""_ns, "https://example.net"_ns)); + EXPECT_FALSE( + PartitioningExceptionList::Check("https://example.com"_ns, ""_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check(""_ns, ""_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.net"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.org"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://example.com"_ns, + "http://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "http://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com."_ns, + "https://example.net"_ns)); + + bool result = PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns); + EXPECT_TRUE(result == (populateList && enableWebcompat)); + } + } + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} + +TEST(TestPartitioningExceptionList, TestPrefWildcard) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com,https://example.net;" + "https://*.foo.com,https://bar.com;" + "https://*.foo.com,https://foobar.net;" + "https://test.net,https://*.example.com;" + "https://test.com,https://*.example.com;" + "https://*.test2.org,*;" + "*,http://notatracker.org"); + + Preferences::SetBool(kPrefEnableWebcompat, true); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://another.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://three.two.foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "https://example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://two.foo.com"_ns, + "http://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://two.foo.com"_ns, + "https://bar.com"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://a.foo.com"_ns, + "https://foobar.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://test.net"_ns, + "https://test.example.com"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check( + "https://test.net"_ns, "https://foo.bar.example.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://test.com"_ns, + "https://foo.test.net"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://one.test2.org"_ns, + "https://example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://two.test2.org"_ns, + "https://foo.example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://three.test2.org"_ns, + "http://example.net"_ns)); + EXPECT_TRUE(PartitioningExceptionList::Check("https://four.sub.test2.org"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://four.sub.test2.com"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("http://four.sub.test2.org"_ns, + "https://bar.com"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check( + "https://four.sub.test2.org."_ns, "https://bar.com"_ns)); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "http://notatracker.org"_ns)); + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} + +TEST(TestPartitioningExceptionList, TestInvalidEntries) +{ + nsAutoCString oldPartitioningExceptionList; + Preferences::GetCString(kPrefEnableWebcompat, oldPartitioningExceptionList); + bool oldEnableWebcompat = Preferences::GetBool(kPrefEnableWebcompat); + + Preferences::SetBool(kPrefEnableWebcompat, true); + + // Empty entries. + Preferences::SetCString(kPrefPartitioningExceptionList, ";;;,;"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + // Schemeless entries. + Preferences::SetCString(kPrefPartitioningExceptionList, + "example.com,example.net"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + // Invalid entry should be skipped and not break other entries. + Preferences::SetCString(kPrefPartitioningExceptionList, + "*,*;" + "https://example.com,https://example.net;" + "http://example.org,"); + + EXPECT_TRUE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("https://foo.com"_ns, + "https://bar.net"_ns)); + + // Unsupported schemes should not be accepted. + Preferences::SetCString(kPrefPartitioningExceptionList, + "ftp://example.com,ftp://example.net;"); + + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + EXPECT_FALSE(PartitioningExceptionList::Check("ftp://example.com"_ns, + "ftp://example.net"_ns)); + + // Test invalid origins with trailing '/'. + Preferences::SetCString(kPrefPartitioningExceptionList, + "https://example.com/,https://example.net/"); + EXPECT_FALSE(PartitioningExceptionList::Check("https://example.com"_ns, + "https://example.net"_ns)); + + Preferences::SetCString(kPrefPartitioningExceptionList, + oldPartitioningExceptionList); + Preferences::SetBool(kPrefEnableWebcompat, oldEnableWebcompat); +} diff --git a/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp new file mode 100644 index 0000000000..44959e4dc8 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestStoragePrincipalHelper.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsICookieJarSettings.h" +#include "nsILoadInfo.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StoragePrincipalHelper.h" + +using mozilla::Preferences; +using namespace mozilla; + +/** + * Creates a test channel with CookieJarSettings which have a partitionKey set. + */ +nsresult CreateMockChannel(nsIPrincipal* aPrincipal, bool isThirdParty, + const nsACString& aPartitionKey, + nsIChannel** aChannel, + nsICookieJarSettings** aCookieJarSettings) { + nsCOMPtr<nsIURI> mockUri; + nsresult rv = NS_NewURI(getter_AddRefs(mockUri), "http://example.com"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsIIOService> service = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = service->NewChannelFromURI(mockUri, nullptr, aPrincipal, aPrincipal, 0, + nsContentPolicyType::TYPE_OTHER, + getter_AddRefs(mockChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> mockLoadInfo = mockChannel->LoadInfo(); + rv = mockLoadInfo->SetIsThirdPartyContextToTopWindow(isThirdParty); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICookieJarSettings> cjs; + rv = mockLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> partitionKeyUri; + rv = NS_NewURI(getter_AddRefs(partitionKeyUri), aPartitionKey); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cjs->InitWithURI(partitionKeyUri, false); + NS_ENSURE_SUCCESS(rv, rv); + + cjs.forget(aCookieJarSettings); + mockChannel.forget(aChannel); + return NS_OK; +} + +TEST(TestStoragePrincipalHelper, TestCreateContentPrincipal) +{ + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, false, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> storagePrincipal; + rv = StoragePrincipalHelper::Create(mockChannel, contentPrincipal, true, + getter_AddRefs(storagePrincipal)); + ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal"; + EXPECT_TRUE(storagePrincipal); + + nsCOMPtr<nsIPrincipal> storagePrincipalSW; + rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + contentPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW)); + ASSERT_EQ(rv, NS_OK) << "Should not fail for ContentPrincipal"; + EXPECT_TRUE(storagePrincipalSW); +} + +TEST(TestStoragePrincipalHelper, TestCreateNullPrincipal) +{ + RefPtr<NullPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + EXPECT_TRUE(nullPrincipal); + + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + nullPrincipal, false, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> storagePrincipal; + rv = StoragePrincipalHelper::Create(mockChannel, nullPrincipal, true, + getter_AddRefs(storagePrincipal)); + EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal"; + EXPECT_FALSE(storagePrincipal); + + nsCOMPtr<nsIPrincipal> storagePrincipalSW; + rv = StoragePrincipalHelper::CreatePartitionedPrincipalForServiceWorker( + nullPrincipal, cookieJarSettings, getter_AddRefs(storagePrincipalSW)); + EXPECT_NS_FAILED(rv) << "Should fail for NullPrincipal"; + EXPECT_FALSE(storagePrincipal); +} + +TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior4) +{ + Preferences::SetInt("network.cookie.cookieBehavior", 4); + + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + for (auto isThirdParty : {false, true}) { + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, isThirdParty, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> testPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get regular principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::ePartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + + // We should always get regular principal if the dFPI is disabled. + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + // Note that we don't test eStorageAccessPrincipal here because it's hard to + // setup the right state for the storage access in gTest. + } +} + +TEST(TestStoragePrincipalHelper, TestGetPrincipalCookieBehavior5) +{ + Preferences::SetInt("network.cookie.cookieBehavior", 5); + + nsCOMPtr<nsIPrincipal> contentPrincipal = + BasePrincipal::CreateContentPrincipal("https://example.com"_ns); + EXPECT_TRUE(contentPrincipal); + + for (auto isThirdParty : {false, true}) { + nsCOMPtr<nsIChannel> mockChannel; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = CreateMockChannel( + contentPrincipal, isThirdParty, "https://example.org"_ns, + getter_AddRefs(mockChannel), getter_AddRefs(cookieJarSettings)); + ASSERT_EQ(rv, NS_OK) << "Could not create a mock channel"; + + nsCOMPtr<nsIPrincipal> testPrincipal; + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eRegularPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get regular principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::ePartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get partitioned principal"; + EXPECT_TRUE(testPrincipal); + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + + // We should always get regular principal if the dFPI is disabled. + rv = StoragePrincipalHelper::GetPrincipal( + mockChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, + getter_AddRefs(testPrincipal)); + ASSERT_EQ(rv, NS_OK) << "Could not get foreign partitioned principal"; + EXPECT_TRUE(testPrincipal); + if (isThirdParty) { + EXPECT_TRUE( + testPrincipal->OriginAttributesRef().mPartitionKey.EqualsLiteral( + "(https,example.org)")); + } else { + EXPECT_TRUE(testPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()); + } + + // Note that we don't test eStorageAccessPrincipal here because it's hard to + // setup the right state for the storage access in gTest. + } +} diff --git a/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp new file mode 100644 index 0000000000..3f77479f74 --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/TestURLQueryStringStripper.cpp @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/Components.h" +#include "nsIURLQueryStringStripper.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsStringFwd.h" + +#include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/URLQueryStringStripper.h" + +using namespace mozilla; + +static const char kPrefQueryStrippingEnabled[] = + "privacy.query_stripping.enabled"; +static const char kPrefQueryStrippingEnabledPBM[] = + "privacy.query_stripping.enabled.pbmode"; +static const char kPrefQueryStrippingList[] = + "privacy.query_stripping.strip_list"; + +/** + * Waits for the strip list in the URLQueryStringStripper to match aExpected. + */ +void waitForStripListChange(const nsACString& aExpected) { + nsresult rv; + nsCOMPtr<nsIURLQueryStringStripper> queryStripper = + components::URLQueryStringStripper::Service(&rv); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil( + "TestURLQueryStringStripper waitForStripListChange"_ns, [&]() -> bool { + nsAutoCString stripList; + rv = queryStripper->TestGetStripList(stripList); + return NS_SUCCEEDED(rv) && stripList.Equals(aExpected); + })); +} + +void DoTest(const nsACString& aTestURL, const bool aIsPBM, + const nsACString& aExpectedURL, uint32_t aExpectedResult) { + nsCOMPtr<nsIURI> testURI; + + NS_NewURI(getter_AddRefs(testURI), aTestURL); + + nsresult rv; + nsCOMPtr<nsIURLQueryStringStripper> queryStripper = + components::URLQueryStringStripper::Service(&rv); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIURI> strippedURI; + uint32_t numStripped; + rv = queryStripper->Strip(testURI, aIsPBM, getter_AddRefs(strippedURI), + &numStripped); + EXPECT_TRUE(NS_SUCCEEDED(rv)); + + EXPECT_TRUE(numStripped == aExpectedResult); + + if (!numStripped) { + EXPECT_TRUE(!strippedURI); + } else { + EXPECT_TRUE(strippedURI->GetSpecOrDefault().Equals(aExpectedURL)); + } +} + +TEST(TestURLQueryStringStripper, TestPrefDisabled) +{ + // Disable the query string stripping by the pref and make sure the stripping + // is disabled. + // Note that we don't need to run a dummy test to create the + // URLQueryStringStripper here because the stripper will never be created if + // the query stripping is disabled. + Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz"); + Preferences::SetBool(kPrefQueryStrippingEnabled, false); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false); + + for (bool isPBM : {false, true}) { + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0); + } +} + +TEST(TestURLQueryStringStripper, TestEmptyStripList) +{ + // Make sure there is no error if the strip list is empty. + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true); + + // To create the URLQueryStringStripper, we need to run a dummy test after + // the query stripping is enabled. By doing this, the stripper will be + // initiated and we are good to test. + DoTest("https://example.com/"_ns, false, ""_ns, 0); + + // Set the strip list to empty and wait until the pref setting is set to the + // stripper. + Preferences::SetCString(kPrefQueryStrippingList, ""); + + waitForStripListChange(""_ns); + + for (bool isPBM : {false, true}) { + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, ""_ns, 0); + } +} + +TEST(TestURLQueryStringStripper, TestStripping) +{ + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, true); + DoTest("https://example.com/"_ns, false, ""_ns, 0); + + Preferences::SetCString(kPrefQueryStrippingList, "fooBar foobaz"); + waitForStripListChange("foobar foobaz"_ns); + + // Test all pref combinations. + for (bool pref : {false, true}) { + for (bool prefPBM : {false, true}) { + Preferences::SetBool(kPrefQueryStrippingEnabled, pref); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, prefPBM); + + // If the service is enabled with the given pref config we need for the + // list changes to propagate as they happen async. + if (pref || prefPBM) { + waitForStripListChange("foobar foobaz"_ns); + } + + // Test with normal and private browsing mode. + for (bool isPBM : {false, true}) { + bool expectStrip = (prefPBM && isPBM) || (pref && !isPBM); + + DoTest("https://example.com/"_ns, isPBM, ""_ns, 0); + DoTest("https://example.com/?Barfoo=123"_ns, isPBM, ""_ns, 0); + + DoTest("https://example.com/?fooBar=123"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 1 : 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 2 : 0); + DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz"_ns, isPBM, + "https://example.com/?Barfoo=456"_ns, expectStrip ? 2 : 0); + + DoTest("https://example.com/?FOOBAR=123"_ns, isPBM, + "https://example.com/"_ns, expectStrip ? 1 : 0); + DoTest("https://example.com/?barfoo=foobar"_ns, isPBM, + "https://example.com/?barfoo=foobar"_ns, 0); + DoTest("https://example.com/?foobar=123&nostrip=456&FooBar=789"_ns, + isPBM, "https://example.com/?nostrip=456"_ns, + expectStrip ? 2 : 0); + DoTest("https://example.com/?AfoobazB=123"_ns, isPBM, + "https://example.com/?AfoobazB=123"_ns, 0); + } + } + } + + // Change the strip list pref to see if it is updated properly. + // We test this in normal browsing, so set the prefs accordingly. + Preferences::SetBool(kPrefQueryStrippingEnabled, true); + Preferences::SetBool(kPrefQueryStrippingEnabledPBM, false); + + Preferences::SetCString(kPrefQueryStrippingList, "Barfoo bazfoo"); + + waitForStripListChange("barfoo bazfoo"_ns); + + DoTest("https://example.com/?fooBar=123"_ns, false, ""_ns, 0); + DoTest("https://example.com/?fooBar=123&foobaz"_ns, false, ""_ns, 0); + + DoTest("https://example.com/?bazfoo=123"_ns, false, "https://example.com/"_ns, + 1); + DoTest("https://example.com/?fooBar=123&Barfoo=456&foobaz=abc"_ns, false, + "https://example.com/?fooBar=123&foobaz=abc"_ns, 1); +} diff --git a/toolkit/components/antitracking/test/gtest/moz.build b/toolkit/components/antitracking/test/gtest/moz.build new file mode 100644 index 0000000000..a7e8595c5e --- /dev/null +++ b/toolkit/components/antitracking/test/gtest/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "TestPartitioningExceptionList.cpp", + "TestStoragePrincipalHelper.cpp", + "TestURLQueryStringStripper.cpp", +] + +LOCAL_INCLUDES += [ + "/xpcom/tests/gtest", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/antitracking/test/xpcshell/data/font.woff b/toolkit/components/antitracking/test/xpcshell/data/font.woff Binary files differnew file mode 100644 index 0000000000..acda4f3d9f --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/data/font.woff diff --git a/toolkit/components/antitracking/test/xpcshell/head.js b/toolkit/components/antitracking/test/xpcshell/head.js new file mode 100644 index 0000000000..f9bf797641 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/head.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../../../../components/url-classifier/tests/unit/head_urlclassifier.js */ + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); diff --git a/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js new file mode 100644 index 0000000000..4063d067f5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_ExceptionListService.js @@ -0,0 +1,107 @@ +// This test ensures that the URL decoration annotations service works as +// expected, and also we successfully downgrade document.referrer to the +// eTLD+1 URL when tracking identifiers controlled by this service are +// present in the referrer URI. + +"use strict"; + +/* Unit tests for the nsIPartitioningExceptionListService implementation. */ + +const { RemoteSettings } = ChromeUtils.importESModule( + "resource://services-settings/remote-settings.sys.mjs" +); + +const COLLECTION_NAME = "partitioning-exempt-urls"; +const PREF_NAME = "privacy.restrict3rdpartystorage.skip_list"; + +do_get_profile(); + +class UpdateEvent extends EventTarget {} +function waitForEvent(element, eventName) { + return new Promise(function (resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +add_task(async _ => { + let peuService = Cc[ + "@mozilla.org/partitioning/exception-list-service;1" + ].getService(Ci.nsIPartitioningExceptionListService); + + // Make sure we have a pref initially, since the exception list service + // requires it. + Services.prefs.setStringPref(PREF_NAME, ""); + + let updateEvent = new UpdateEvent(); + let records = [ + { + id: "1", + last_modified: 1000000000000001, + firstPartyOrigin: "https://example.org", + thirdPartyOrigin: "https://tracking.example.com", + }, + ]; + + // Add some initial data + let db = RemoteSettings(COLLECTION_NAME).db; + await db.importChanges({}, Date.now(), records); + + let promise = waitForEvent(updateEvent, "update"); + let obs = data => { + let event = new CustomEvent("update", { detail: data }); + updateEvent.dispatchEvent(event); + }; + peuService.registerAndRunExceptionListObserver(obs); + let list = await promise; + Assert.equal(list, "", "No items in the list"); + + // Second event is from the RemoteSettings record. + list = await waitForEvent(updateEvent, "update"); + Assert.equal( + list, + "https://example.org,https://tracking.example.com", + "Has one item in the list" + ); + + records.push({ + id: "2", + last_modified: 1000000000000002, + firstPartyOrigin: "https://foo.org", + thirdPartyOrigin: "https://bar.com", + }); + + promise = waitForEvent(updateEvent, "update"); + await RemoteSettings(COLLECTION_NAME).emit("sync", { + data: { current: records }, + }); + list = await promise; + Assert.equal( + list, + "https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref(PREF_NAME, "https://test.com,https://test3.com"); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + promise = waitForEvent(updateEvent, "update"); + Services.prefs.setStringPref( + PREF_NAME, + "https://test.com,https://test3.com;https://abc.com,https://def.com" + ); + list = await promise; + Assert.equal( + list, + "https://test.com,https://test3.com;https://abc.com,https://def.com;https://example.org,https://tracking.example.com;https://foo.org,https://bar.com", + "Has several items in the list" + ); + + peuService.unregisterExceptionListObserver(obs); + await db.clear(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js new file mode 100644 index 0000000000..3ce1f8bfb7 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_cookie_behavior.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const PREF_FPI = "privacy.firstparty.isolate"; +const PREF_COOKIE_BEHAVIOR = "network.cookie.cookieBehavior"; +const PREF_COOKIE_BEHAVIOR_PBMODE = "network.cookie.cookieBehavior.pbmode"; + +registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_FPI); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); +}); + +add_task(function test_FPI_off() { + Services.prefs.setBoolPref(PREF_FPI, false); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal(Services.cookies.getCookieBehavior(false), i); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal(Services.cookies.getCookieBehavior(true), i); + } +}); + +add_task(function test_FPI_on() { + Services.prefs.setBoolPref(PREF_FPI, true); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR), i); + equal( + Services.cookies.getCookieBehavior(false), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR); + + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + equal(Services.prefs.getIntPref(PREF_COOKIE_BEHAVIOR_PBMODE), i); + equal( + Services.cookies.getCookieBehavior(true), + i == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ? Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + : i + ); + } + + Services.prefs.clearUserPref(PREF_FPI); +}); + +add_task(function test_private_cookieBehavior_mirroring() { + // Test that the private cookieBehavior getter will return the regular pref if + // the regular pref has a user value and the private pref has a default value. + Services.prefs.clearUserPref(PREF_COOKIE_BEHAVIOR_PBMODE); + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR)) { + continue; + } + + equal(Services.cookies.getCookieBehavior(true), i); + } + + // Test that the private cookieBehavior getter will always return the private + // pref if the private cookieBehavior has a user value. + for (let i = 0; i <= Ci.nsICookieService.BEHAVIOR_LAST; ++i) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR_PBMODE, i); + if (!Services.prefs.prefHasUserValue(PREF_COOKIE_BEHAVIOR_PBMODE)) { + continue; + } + + for (let j = 0; j <= Ci.nsICookieService.BEHAVIOR_LAST; ++j) { + Services.prefs.setIntPref(PREF_COOKIE_BEHAVIOR, j); + + equal(Services.cookies.getCookieBehavior(true), i); + } + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js new file mode 100644 index 0000000000..cdf2cdec2c --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_getPartitionKeyFromURL.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +const TEST_CASES = [ + // Tests for different schemes. + { + url: "http://example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for sub domains + { + url: "http://sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://sub.sub.example.com/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for path and query. + { + url: "http://www.example.com/path/to/somewhere/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "http://www.example.com/?query=string", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for other ports. + { + url: "http://example.com:8080/", + partitionKeySite: "(http,example.com)", + partitionKeyWithoutSite: "example.com", + }, + { + url: "https://example.com:8080/", + partitionKeySite: "(https,example.com)", + partitionKeyWithoutSite: "example.com", + }, + // Tests for about urls + { + url: "about:about", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + { + url: "about:preferences", + partitionKeySite: + "(about,about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla)", + partitionKeyWithoutSite: + "about.ef2a7dd5-93bc-417f-a698-142c3116864f.mozilla", + }, + // Test for ip addresses + { + url: "http://127.0.0.1/", + partitionKeySite: "(http,127.0.0.1)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://127.0.0.1:8080/", + partitionKeySite: "(http,127.0.0.1,8080)", + partitionKeyWithoutSite: "127.0.0.1", + }, + { + url: "http://[2001:db8::ff00:42:8329]", + partitionKeySite: "(http,[2001:db8::ff00:42:8329])", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + { + url: "http://[2001:db8::ff00:42:8329]:8080", + partitionKeySite: "(http,[2001:db8::ff00:42:8329],8080)", + partitionKeyWithoutSite: "[2001:db8::ff00:42:8329]", + }, + // Tests for moz-extension + { + url: "moz-extension://bafa4a3f-5c49-48d6-9788-03489419b70e", + partitionKeySite: "", + partitionKeyWithoutSite: "", + }, + // Tests for non tld + { + url: "http://notld", + partitionKeySite: "(http,notld)", + partitionKeyWithoutSite: "notld", + }, + { + url: "http://com", + partitionKeySite: "(http,com)", + partitionKeyWithoutSite: "com", + }, + { + url: "http://com:8080", + partitionKeySite: "(http,com,8080)", + partitionKeyWithoutSite: "com", + }, +]; + +const TEST_INVALID_URLS = [ + "", + "/foo", + "An invalid URL", + "https://", + "http:///", + "http://foo:bar", +]; + +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); + + Assert.equal( + partitionKey, + test.partitionKeySite, + "The partitionKey is correct." + ); + } +}); + +add_task(async function test_get_partition_key_from_url_without_site() { + Services.prefs.setBoolPref("privacy.dynamic_firstparty.use_site", false); + + for (const test of TEST_CASES) { + info(`Testing url: ${test.url}`); + let partitionKey = ChromeUtils.getPartitionKeyFromURL(test.url); + + Assert.equal( + partitionKey, + test.partitionKeyWithoutSite, + "The partitionKey is correct." + ); + } + + Services.prefs.clearUserPref("privacy.dynamic_firstparty.use_site"); +}); + +add_task(async function test_blob_url() { + do_get_profile(); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "<h1>Hello!</h1>"; + response.write(body); + }); + + server.registerPathHandler("/iframe", (metadata, response) => { + var body = ` + <script> + var blobUrl = URL.createObjectURL(new Blob([])); + parent.postMessage(blobUrl, "http://example.org"); + </script> + `; + response.write(body); + }); + + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + let blobUrl = await contentPage.spawn([], async () => { + // Create a third-party iframe and create a blob url in there. + let f = this.content.document.createElement("iframe"); + f.src = "http://foo.com/iframe"; + + let blob_url = await new Promise(resolve => { + this.content.addEventListener("message", event => resolve(event.data), { + once: true, + }); + this.content.document.body.append(f); + }); + + return blob_url; + }); + + let partitionKey = ChromeUtils.getPartitionKeyFromURL(blobUrl); + + // 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 + // partitionKey. And ChromeUtils.getPartitionKeyFromURL() will get + // partitionKey from that principal. So, we will get an empty partitionKey + // here. + // XXX: The behavior here is debatable. + Assert.equal(partitionKey, "", "The partitionKey of blob url is correct."); + + await contentPage.close(); +}); + +add_task(async function test_throw_with_invalid_URL() { + // The API should throw if the url is invalid. + for (const invalidURL of TEST_INVALID_URLS) { + info(`Testing invalid url: ${invalidURL}`); + + Assert.throws( + () => { + ChromeUtils.getPartitionKeyFromURL(invalidURL); + }, + /NS_ERROR_MALFORMED_URI/, + "It should fail on invalid URLs." + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js new file mode 100644 index 0000000000..84694a35d5 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers.js @@ -0,0 +1,523 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const TRACKING_PAGE2 = + "https://tracking.example.org^partitionKey=(https,example.com)"; +const BENIGN_PAGE = "https://example.com"; +const FOREIGN_PAGE = "https://example.net"; +const FOREIGN_PAGE2 = "https://example.net^partitionKey=(https,example.com)"; +const FOREIGN_PAGE3 = "https://example.net^partitionKey=(https,example.org)"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +async function setupTest(aCookieBehavior) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", aCookieBehavior); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setCharPref("privacy.purge_trackers.logging.level", "Debug"); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +} + +/** + * Test that purging doesn't happen when it shouldn't happen. + */ +add_task(async function testNotPurging() { + await UrlClassifierTestUtils.addTestTrackers(); + setupTest(Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", false); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true); + Services.prefs.setBoolPref("privacy.clearOnShutdown.history", true); + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie remains."); + Services.prefs.clearUserPref("privacy.sanitize.sanitizeOnShutdown"); + Services.prefs.clearUserPref("privacy.clearOnShutdown.history"); + + await PurgeTrackerService.purgeTrackingCookieJars(); + ok(!SiteDataTestUtils.hasCookies(TRACKING_PAGE), "cookie cleared."); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test that cookies indexedDB and localStorage are purged if the cookie is found + * on the tracking list and does not have an Interaction Permission. + */ +async function testIndexedDBAndLocalStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + `A non-tracking foreign page should retain cookies after purging` + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + `localStorage for ${url} should not have been removed.` + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Cookie should have been removed. + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed" + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are treated based on their base domain, not origin. + */ +async function testBaseDomain() { + await UrlClassifierTestUtils.addTestTrackers(); + + let associatedOrigins = [ + "https://itisatracker.org", + "https://sub.itisatracker.org", + "https://www.itisatracker.org", + "https://sub.sub.sub.itisatracker.org", + "http://itisatracker.org", + "http://sub.itisatracker.org", + ]; + + for (let permissionOrigin of associatedOrigins) { + // Only one of the associated origins gets permission, but + // all should be exempt from purging. + PermissionTestUtils.add( + permissionOrigin, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + for (let origin of associatedOrigins) { + SiteDataTestUtils.addToCookies({ origin }); + } + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let origin of associatedOrigins) { + ok( + SiteDataTestUtils.hasCookies(origin), + `${origin} should have retained its cookies when permission is set for ${permissionOrigin}.` + ); + } + + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + + PermissionTestUtils.remove(permissionOrigin, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that trackers are not cleared if they are associated + * with an entry on the entity list that has user interaction. + */ +async function testUserInteraction(ownerPage) { + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + true + ); + // The test URL for the entity list for annotation is + // itisatrap.org/?resource=example.org, so we need to + // add example.org as a tracker. + Services.prefs.setCharPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "example.org" + ); + await UrlClassifierTestUtils.addTestTrackers(); + + // example.org and itisatrap.org are hard coded test values on the entity list. + const RESOURCE_PAGE = "https://example.org"; + + PermissionTestUtils.add( + ownerPage, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + SiteDataTestUtils.addToCookies({ origin: RESOURCE_PAGE }); + + // Add another tracker to verify we're actually purging. + SiteDataTestUtils.addToCookies({ + origin: "https://another-tracking.example.net", + }); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should have retained its cookies when permission is set for ${ownerPage}.` + ); + + ok( + !SiteDataTestUtils.hasCookies("https://another-tracking.example.net"), + "cookie is removed after purge with no storage access permission." + ); + + Services.prefs.setBoolPref( + "privacy.purge_trackers.consider_entity_list", + false + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + !SiteDataTestUtils.hasCookies(RESOURCE_PAGE), + `${RESOURCE_PAGE} should not have retained its cookies when permission is set for ${ownerPage} and the entity list pref is off.` + ); + + PermissionTestUtils.remove(ownerPage, "storageAccessAPI"); + await SiteDataTestUtils.clear(); + + Services.prefs.clearUserPref("privacy.purge_trackers.consider_entity_list"); + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that quota storage (even without cookies) is considered when purging trackers. + */ +async function testQuotaStorage() { + await UrlClassifierTestUtils.addTestTrackers(); + + let testCases = [ + { localStorage: true, indexedDB: true }, + { localStorage: false, indexedDB: true }, + { localStorage: true, indexedDB: false }, + ]; + + for (let { localStorage, indexedDB } of testCases) { + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION + ); + + if (localStorage) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + } + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + await SiteDataTestUtils.addToIndexedDB(url); + } + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + } + + if (indexedDB) { + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + } + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + if (localStorage) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed for non-tracking page." + ); + } + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should have been removed." + ); + } + } + + if (indexedDB) { + for (let url of [ + BENIGN_PAGE, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage for non-tracking page was not deleted" + ); + } + + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + } + + await SiteDataTestUtils.clear(); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +/** + * Test that we correctly delete cookies and storage for sites + * with an expired interaction permission. + */ +async function testExpiredInteractionPermission() { + await UrlClassifierTestUtils.addTestTrackers(); + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + 500 + ); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + SiteDataTestUtils.addToLocalStorage(url); + SiteDataTestUtils.addToCookies({ origin: url }); + await SiteDataTestUtils.addToIndexedDB(url); + } + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + for (let url of [ + TRACKING_PAGE, + TRACKING_PAGE2, + FOREIGN_PAGE, + FOREIGN_PAGE2, + FOREIGN_PAGE3, + ]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + `We have data for ${url}` + ); + } + + // Run purge after storage access permission has been removed. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 500)); + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + for (let url of [TRACKING_PAGE, TRACKING_PAGE2]) { + ok( + !SiteDataTestUtils.hasCookies(url), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(url), + 0, + "quota storage was deleted" + ); + } + + // Cookie should not have been removed. + for (let url of [FOREIGN_PAGE, FOREIGN_PAGE2, FOREIGN_PAGE3]) { + ok( + SiteDataTestUtils.hasCookies(url), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(url), + "localStorage should not have been removed while storage access permission exists." + ); + } + + UrlClassifierTestUtils.cleanupTestTrackers(); +} + +add_task(async function () { + const cookieBehaviors = [ + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER, + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ]; + + for (let cookieBehavior of cookieBehaviors) { + await setupTest(cookieBehavior); + await testIndexedDBAndLocalStorage(); + await testBaseDomain(); + // example.org and itisatrap.org are hard coded test values on the entity list. + await testUserInteraction("https://itisatrap.org"); + await testUserInteraction( + "https://itisatrap.org^firstPartyDomain=example.net" + ); + await testQuotaStorage(); + await testExpiredInteractionPermission(); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js new file mode 100644 index 0000000000..a1502373dc --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_purge_trackers_telemetry.js @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TRACKING_PAGE = "https://tracking.example.org"; +const BENIGN_PAGE = "https://example.com"; + +const { UrlClassifierTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/UrlClassifierTestUtils.sys.mjs" +); +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "PurgeTrackerService", + "@mozilla.org/purge-tracker-service;1", + "nsIPurgeTrackerService" +); + +add_task(async function setup() { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER + ); + Services.prefs.setBoolPref("privacy.purge_trackers.enabled", true); + Services.prefs.setStringPref( + "urlclassifier.trackingAnnotationTable.testEntries", + "tracking.example.org" + ); + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + // Enables us to test localStorage in xpcshell. + Services.prefs.setBoolPref("dom.storage.client_validation", false); +}); + +/** + * Test telemetry for cookie purging. + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + let FIVE_DAYS = 5 * 24 * 60 * 60 * 1000; + + PermissionTestUtils.add( + TRACKING_PAGE, + "storageAccessAPI", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_TIME, + Date.now() + FIVE_DAYS + ); + + SiteDataTestUtils.addToLocalStorage(TRACKING_PAGE); + SiteDataTestUtils.addToCookies({ origin: BENIGN_PAGE }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE }); + await SiteDataTestUtils.addToIndexedDB(TRACKING_PAGE); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + let notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + let remainingDaysHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS" + ); + let intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Purge while storage access permission exists. + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie remains while storage access permission exists." + ); + ok( + SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.greater( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + `We have data for ${TRACKING_PAGE}` + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 0, 1); + TelemetryTestUtils.assertHistogram(notPurgedHistogram, 1, 1); + TelemetryTestUtils.assertHistogram(remainingDaysHistogram, 4, 2); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + notPurgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION" + ); + intervalHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_INTERVAL_HOURS" + ); + + // Run purge after storage access permission has been removed. + PermissionTestUtils.remove(TRACKING_PAGE, "storageAccessAPI"); + await PurgeTrackerService.purgeTrackingCookieJars(); + + ok( + SiteDataTestUtils.hasCookies(BENIGN_PAGE), + "A non-tracking page should retain cookies after purging" + ); + + // Cookie should have been removed. + ok( + !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge with no storage access permission." + ); + ok( + !SiteDataTestUtils.hasLocalStorage(TRACKING_PAGE), + "localStorage should not have been removed while storage access permission exists." + ); + Assert.equal( + await SiteDataTestUtils.getQuotaUsage(TRACKING_PAGE), + 0, + "quota storage was deleted" + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + Assert.equal( + notPurgedHistogram.snapshot().sum, + 0, + "no origins with user interaction" + ); + TelemetryTestUtils.assertHistogram(intervalHistogram, 0, 1); + + UrlClassifierTestUtils.cleanupTestTrackers(); +}); + +/** + * Test counting correctly across cookies batches + */ +add_task(async function () { + await UrlClassifierTestUtils.addTestTrackers(); + + // Enforce deleting the same origin twice by adding two cookies and setting + // the max number of cookies per batch to 1. + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie1" }); + SiteDataTestUtils.addToCookies({ origin: TRACKING_PAGE, name: "cookie2" }); + Services.prefs.setIntPref("privacy.purge_trackers.max_purge_count", 1); + + let purgedHistogram = TelemetryTestUtils.getAndClearHistogram( + "COOKIE_PURGING_ORIGINS_PURGED" + ); + + await PurgeTrackerService.purgeTrackingCookieJars(); + + // Cookie should have been removed. + await TestUtils.waitForCondition( + () => !SiteDataTestUtils.hasCookies(TRACKING_PAGE), + "cookie is removed after purge." + ); + + TelemetryTestUtils.assertHistogram(purgedHistogram, 1, 1); + + Services.prefs.clearUserPref("privacy.purge_trackers.max_purge_count"); + UrlClassifierTestUtils.cleanupTestTrackers(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js new file mode 100644 index 0000000000..97e95a43f4 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_rejectForeignAllowList.js @@ -0,0 +1,116 @@ +"use strict"; + +do_get_profile(); + +// Let's use XPCShellContentUtils to open/close tabs. +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +var createHttpServer = (...args) => { + return XPCShellContentUtils.createHttpServer(...args); +}; + +const server = createHttpServer({ + hosts: ["3rdparty.org", "4thparty.org", "foobar.com"], +}); + +async function testThings(prefValue, expected) { + await new Promise(resolve => + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_ALL_CACHES, + resolve + ) + ); + + Services.prefs.setCharPref("privacy.rejectForeign.allowList", prefValue); + + let cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyChannel", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(`<html><img src="http://3rdparty.org/img" /></html>`); + }); + + server.registerPathHandler("/img", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "image/png", false); + response.write("Not an image"); + }); + }); + + // Let's load 3rdparty.org as a 3rd-party. + let contentPage = await XPCShellContentUtils.loadContentPage( + "http://foobar.com/test3rdPartyChannel" + ); + Assert.equal(await cookiePromise, expected, "Cookies received?"); + await contentPage.close(); + + cookiePromise = new Promise(resolve => { + server.registerPathHandler("/test3rdPartyDocument", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write( + `<html><iframe src="http://3rdparty.org/iframe" /></html>` + ); + }); + + server.registerPathHandler("/iframe", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(`<html><img src="http://4thparty.org/img" /></html>`); + }); + + server.registerPathHandler("/img", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + resolve(request.hasHeader("Cookie") ? request.getHeader("Cookie") : ""); + response.setHeader("Content-Type", "image/png", false); + response.write("Not an image"); + }); + }); + + // Let's load 3rdparty.org loading a 4th-party. + contentPage = await XPCShellContentUtils.loadContentPage( + "http://foobar.com/test3rdPartyDocument" + ); + Assert.equal(await cookiePromise, expected, "Cookies received?"); + await contentPage.close(); +} + +add_task(async function test_rejectForeignAllowList() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 1); + Services.prefs.setBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled", + true + ); + + // We don't want to have 'secure' cookies because our test http server doesn't run in https. + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + false + ); + + server.registerPathHandler("/setCookies", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.setHeader("Set-Cookie", "cookie=wow; sameSite=none", true); + response.write("<html></html>"); + }); + + // Let's set a cookie. + let contentPage = await XPCShellContentUtils.loadContentPage( + "http://3rdparty.org/setCookies" + ); + await contentPage.close(); + Assert.equal(Services.cookies.cookies.length, 1); + + // Without exceptionlisting, no cookies should be shared. + await testThings("", ""); + + // Let's exceptionlist 3rdparty.org + await testThings("3rdparty.org", "cookie=wow"); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js new file mode 100644 index 0000000000..0492f5ff2a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_authhttp.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +function Requestor() {} +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth(channel, level, authInfo) { + Assert.equal("secret", authInfo.realm); + // No passwords in the URL -> nothing should be prefilled + Assert.equal(authInfo.username, ""); + Assert.equal(authInfo.password, ""); + Assert.equal(authInfo.domain, ""); + + authInfo.username = "guest"; + authInfo.password = "guest"; + + return true; + }, + + asyncPromptAuth(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +let observer = channel => { + if ( + !(channel instanceof Ci.nsIHttpChannel && channel.URI.host === "localhost") + ) { + return; + } + channel.notificationCallbacks = new Requestor(); +}; +Services.obs.addObserver(observer, "http-on-modify-request"); + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + + for (let test of [true, false]) { + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref("privacy.partition.network_state", test); + + const httpserv = new HttpServer(); + httpserv.registerPathHandler("/auth", (metadata, response) => { + // btoa("guest:guest"), but that function is not available here + const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + let body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); + }); + + httpserv.start(-1); + const URL = "http://localhost:" + httpserv.identity.primaryPort; + + const httpHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" + ].getService(Ci.nsIHttpProtocolHandler); + + const contentPage = await CookieXPCShellUtils.loadContentPage( + URL + "/auth?r=" + Math.random() + ); + await contentPage.close(); + + let key; + if (test) { + key = `^partitionKey=%28http%2Clocalhost%2C${httpserv.identity.primaryPort}%29:http://localhost:${httpserv.identity.primaryPort}`; + } else { + key = `:http://localhost:${httpserv.identity.primaryPort}`; + } + + Assert.equal(httpHandler.authCacheKeys.includes(key), true, "Key found!"); + + await new Promise(resolve => httpserv.stop(resolve)); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js new file mode 100644 index 0000000000..ba2f6c6894 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_clientAuthRemember.js @@ -0,0 +1,129 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService( + Ci.nsIClientAuthRememberService +); +let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB +); + +function getOAWithPartitionKey( + { scheme = "https", topLevelBaseDomain, port = null } = {}, + originAttributes = {} +) { + if (!topLevelBaseDomain || !scheme) { + return originAttributes; + } + + return { + ...originAttributes, + partitionKey: `(${scheme},${topLevelBaseDomain}${port ? `,${port}` : ""})`, + }; +} + +// These are not actual server and client certs. The ClientAuthRememberService +// does not care which certs we store decisions for, as long as they're valid. +let [clientCert] = certDB.getCerts(); + +function addSecurityInfo({ host, topLevelBaseDomain, originAttributes = {} }) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + cars.rememberDecisionScriptable(host, attrs, clientCert); +} + +function testSecurityInfo({ + host, + topLevelBaseDomain, + originAttributes = {}, + expected = true, +}) { + let attrs = getOAWithPartitionKey({ topLevelBaseDomain }, originAttributes); + + let messageSuffix = `for ${host}`; + if (topLevelBaseDomain) { + messageSuffix += ` partitioned under ${topLevelBaseDomain}`; + } + + let hasRemembered = cars.hasRememberedDecisionScriptable(host, attrs, {}); + + Assert.equal( + hasRemembered, + expected, + `CAR ${expected ? "is set" : "is not set"} ${messageSuffix}` + ); +} + +function addTestEntries() { + let entries = [ + { host: "example.net" }, + { host: "test.example.net" }, + { host: "example.org" }, + { host: "example.com", topLevelBaseDomain: "example.net" }, + { + host: "test.example.net", + topLevelBaseDomain: "example.org", + }, + { + host: "foo.example.com", + originAttributes: { + privateBrowsingId: 1, + }, + }, + ]; + + info("Add test state"); + entries.forEach(addSecurityInfo); + info("Ensure we have the correct state initially"); + entries.forEach(testSecurityInfo); +} + +add_task(async () => { + addTestEntries(); + + info("Should not be set for unrelated host"); + [undefined, "example.org", "example.net", "example.com"].forEach( + topLevelBaseDomain => + testSecurityInfo({ + host: "mochit.test", + topLevelBaseDomain, + expected: false, + }) + ); + + info("Should not be set for unrelated subdomain"); + testSecurityInfo({ host: "foo.example.net", expected: false }); + + info("Should not be set for unpartitioned first party"); + testSecurityInfo({ + host: "example.com", + expected: false, + }); + + info("Should not be set under different first party"); + testSecurityInfo({ + host: "example.com", + topLevelBaseDomain: "example.org", + expected: false, + }); + testSecurityInfo({ + host: "test.example.net", + topLevelBaseDomain: "example.com", + expected: false, + }); + + info("Should not be set in partitioned context"); + ["example.com", "example.net", "example.org", "mochi.test"].forEach( + topLevelBaseDomain => + testSecurityInfo({ + host: "foo.example.com", + topLevelBaseDomain, + expected: false, + }) + ); + + // Cleanup + cars.clearRememberedDecisions(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js new file mode 100644 index 0000000000..46e230ec3e --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_font.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com", "bar.com"], + }); + + server.registerFile( + "/font.woff", + do_get_file("data/font.woff"), + (_, response) => { + response.setHeader("Access-Control-Allow-Origin", "*", false); + gHits++; + } + ); + + server.registerPathHandler("/font", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + <style type="text/css"> + @font-face { + font-family: foo; + src: url("http://example.org/font.woff") format('woff'); + } + body { font-family: foo } + </style> + <iframe src="http://example.org/font-iframe"> + </iframe>`; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/font-iframe", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + <style type="text/css"> + @font-face { + font-family: foo; + src: url("http://example.org/font.woff") format('woff'); + } + body { font-family: foo } + </style>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 5, + }, + { + prefValue: false, + // The font in page B/C is CORS, the channel will be flagged with + // nsIRequest::LOAD_ANONYMOUS. + // The flag makes the font in A and B/C use different cache key. + hitsCount: 2, + }, + ]; + + for (let test of tests) { + info("Clear network caches"); + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/font" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/font" + ); + await contentPage.close(); + + info("Let's load a page with origin C"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://bar.com/font" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js new file mode 100644 index 0000000000..7492d2267a --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_image.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + server.registerPathHandler("/image.png", (metadata, response) => { + gHits++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + var body = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAEUlEQVQImWP4z8AAQTAamQkAhpcI+DeMzFcAAAAASUVORK5CYII=" + ); + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/image", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<img src="http://example.org/image.png">`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + prefValue: true, + hitsCount: 2, + }, + { + prefValue: false, + hitsCount: 1, + }, + ]; + + for (let test of tests) { + info("Clear image and network caches"); + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(true); // true=chrome + imageCache.clearCache(false); // false=content + Services.cache2.clear(); + + info("Reset the hits count"); + gHits = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/image" + ); + await contentPage.close(); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/image" + ); + await contentPage.close(); + + Assert.equal(gHits, test.hitsCount, "The number of hits match"); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js new file mode 100644 index 0000000000..1215ec2384 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_prefetch.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +// Small red image. +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "image.png" + ); + ok( + foundEntryCount > 0, + `Cache entries expected for image.png and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.prefetch-next", true); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/image.png", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png", false); + response.write(IMG_BYTES); + }); + + server.registerPathHandler("/prefetch", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<html><head></head><body><script> + const link = document.createElement("link") + link.setAttribute("rel", "prefetch"); + link.setAttribute("href", "http://example.org/image.png"); + document.head.appendChild(link); + link.onload = () => { + const img = document.createElement("IMG"); + img.src = "http://example.org/image.png"; + document.body.appendChild(img); + fetch("/done").then(() => {}); + } + </script></body></html>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + originAttributes: { partitionKey: "(http,example.org)" }, + prefValue: true, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/prefetch" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js new file mode 100644 index 0000000000..be61a2e6d4 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_staticPartition_preload.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +let gHints = 0; + +CookieXPCShellUtils.init(this); + +function countMatchingCacheEntries(cacheEntries, domain, path) { + return cacheEntries + .map(entry => entry.uri.asciiSpec) + .filter(spec => spec.includes(domain)) + .filter(spec => spec.includes(path)).length; +} + +async function checkCache(originAttributes) { + const loadContextInfo = Services.loadContextInfo.custom( + false, + originAttributes + ); + + const data = await new Promise(resolve => { + let cacheEntries = []; + let cacheVisitor = { + onCacheStorageInfo(num, consumption) {}, + onCacheEntryInfo(uri, idEnhance) { + cacheEntries.push({ uri, idEnhance }); + }, + onCacheEntryVisitCompleted() { + resolve(cacheEntries); + }, + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + }; + // Visiting the disk cache also visits memory storage so we do not + // need to use Services.cache2.memoryCacheStorage() here. + let storage = Services.cache2.diskCacheStorage(loadContextInfo); + storage.asyncVisitStorage(cacheVisitor, true); + }); + + let foundEntryCount = countMatchingCacheEntries( + data, + "example.org", + "style.css" + ); + ok( + foundEntryCount > 0, + `Cache entries expected for style.css and OA=${originAttributes}` + ); +} + +add_task(async () => { + do_get_profile(); + + Services.prefs.setBoolPref("network.preload", true); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org", "foo.com"], + }); + + server.registerPathHandler("/empty", (metadata, response) => { + var body = "<h1>Hello!</h1>"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/style.css", (metadata, response) => { + gHints++; + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Access-Control-Allow-Origin", "*", false); + var body = "* { color: red }"; + response.bodyOutputStream.write(body, body.length); + }); + + server.registerPathHandler("/preload", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = `<html><head></head><body><script> + const link = document.createElement("link") + link.setAttribute("rel", "preload"); + link.setAttribute("as", "style"); + link.setAttribute("href", "http://example.org/style.css"); + document.head.appendChild(link); + link.onload = () => { + fetch("/done").then(() => {}); + }; + </script></body></html>`; + response.bodyOutputStream.write(body, body.length); + }); + + const tests = [ + { + // 2 hints because we have 2 different top-level origins, loading the + // same resource. This will end up creating 2 separate cache entries. + hints: 2, + prefValue: true, + originAttributes: { partitionKey: "(http,example.org)" }, + }, + { + // 1 hint because, with network-state isolation, the cache entry will be + // reused for the second loading, even if the top-level origins are + // different. + hints: 1, + originAttributes: {}, + prefValue: false, + }, + ]; + + for (let test of tests) { + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); + + info("Reset the shared sheets"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/empty" + ); + + await contentPage.spawn([], () => + // eslint-disable-next-line no-undef + content.windowUtils.clearSharedStyleSheetCache() + ); + + await contentPage.close(); + + info("Reset the counter"); + gHints = 0; + + info("Enabling network state partitioning"); + Services.prefs.setBoolPref( + "privacy.partition.network_state", + test.prefValue + ); + + let complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin A"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + complete = new Promise(resolve => { + server.registerPathHandler("/done", (metadata, response) => { + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + var body = "OK"; + response.bodyOutputStream.write(body, body.length); + resolve(); + }); + }); + + info("Let's load a page with origin B"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "http://foo.com/preload" + ); + + await complete; + await checkCache(test.originAttributes); + await contentPage.close(); + + Assert.equal( + gHints, + test.hints, + "We have the current number of requests with pref " + test.prefValue + ); + } +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js new file mode 100644 index 0000000000..0f6ded1218 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_tracking_db_service.js @@ -0,0 +1,492 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: This test may cause intermittents if run at exactly midnight. + +"use strict"; + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +XPCOMUtils.defineLazyGetter(this, "DB_PATH", function () { + return PathUtils.join(PathUtils.profileDir, "protections.sqlite"); +}); + +const SQL = { + insertCustomTimeEvent: + "INSERT INTO events (type, count, timestamp)" + + "VALUES (:type, :count, date(:timestamp));", + + selectAllEntriesOfType: "SELECT * FROM events WHERE type = :type;", + + selectAll: "SELECT * FROM events", +}; + +// Emulate the content blocking log. We do not record the url key, nor +// do we use the aggregated event number (the last element in the array). +const LOG = { + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + "https://2.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://3.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_CRYPTOMINING_CONTENT, true, 2], + ], + "https://4.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 3], + ], + "https://5.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_TRACKER, true, 1], + ], + // Cookie blocked for other reason, then identified as a tracker + "https://6.example.com": [ + [ + Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_ALL | + Ci.nsIWebProgressListener.STATE_LOADED_LEVEL_1_TRACKING_CONTENT, + true, + 4, + ], + ], + "https://7.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_SOCIALTRACKER, true, 1], + ], + "https://8.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_SOCIALTRACKING_CONTENT, true, 1], + ], + + // The contents below should not add to the database. + // Cookie loaded but not blocked. + "https://10.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED, true, 1], + ], + // Tracker cookie loaded but not blocked. + "https://11.unblocked.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_TRACKER, true, 1], + ], + // Social tracker cookie loaded but not blocked. + "https://12.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_LOADED_SOCIALTRACKER, true, 1], + ], + // Cookie blocked for other reason (not a tracker) + "https://13.example.com": [ + [Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_BY_PERMISSION, true, 2], + ], + // Fingerprinters set to block, but this one has an exception + "https://14.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_FINGERPRINTING_CONTENT, false, 1], + ], + // Two fingerprinters replaced with a shims script, should be treated as blocked + // and increment the counter. + "https://15.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], + "https://16.example.com": [ + [Ci.nsIWebProgressListener.STATE_REPLACED_FINGERPRINTING_CONTENT, true, 1], + ], +}; + +do_get_profile(); + +Services.prefs.setBoolPref("browser.contentblocking.database.enabled", true); +Services.prefs.setBoolPref( + "privacy.socialtracking.block_cookies.enabled", + true +); +Services.prefs.setBoolPref( + "privacy.trackingprotection.fingerprinting.enabled", + true +); +Services.prefs.setBoolPref( + "browser.contentblocking.cfr-milestone.enabled", + true +); +Services.prefs.setIntPref( + "browser.contentblocking.cfr-milestone.update-interval", + 0 +); +Services.prefs.setStringPref( + "browser.contentblocking.cfr-milestone.milestones", + "[1000, 5000, 10000, 25000, 100000, 500000]" +); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.contentblocking.database.enabled"); + Services.prefs.clearUserPref("privacy.socialtracking.block_cookies.enabled"); + Services.prefs.clearUserPref( + "privacy.trackingprotection.fingerprinting.enabled" + ); + Services.prefs.clearUserPref("browser.contentblocking.cfr-milestone.enabled"); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.update-interval" + ); + Services.prefs.clearUserPref( + "browser.contentblocking.cfr-milestone.milestones" + ); +}); + +// This tests that data is added successfully, different types of events should get +// their own entries, when the type is the same they should be aggregated. Events +// that are not blocking events should not be recorded. Cookie blocking events +// should only be recorded if we can identify the cookie as a tracking cookie. +add_task(async function test_save_and_delete() { + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Peek in the DB to make sure we have the right data. + let db = await Sqlite.openConnection({ path: DB_PATH }); + // Make sure the items table was created. + ok(await db.tableExists("events"), "events table exists"); + + // make sure we have the correct contents in the database + let rows = await db.execute(SQL.selectAll); + equal( + rows.length, + 5, + "Events that should not be saved have not been, length is 4" + ); + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 1, "Only one day has had tracker entries, length is 1"); + let count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one tracker entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal(rows.length, 1, "Only one day has had cookies entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 3, "Cookie entries were aggregated"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had cryptominer entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 1, "there is only one cryptominer entry"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 1, + "Only one day has had fingerprinters entries, length is 1" + ); + count = rows[0].getResultByName("count"); + equal(count, 3, "there are three fingerprinter entries"); + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.SOCIAL_ID, + }); + equal(rows.length, 1, "Only one day has had social entries, length is 1"); + count = rows[0].getResultByName("count"); + equal(count, 2, "there are two social entries"); + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +// This tests that content blocking events encountered on the same day get aggregated, +// and those on different days get seperate entries +add_task(async function test_timestamp_aggragation() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + let today = new Date().toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 4, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: yesterday, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 1, + timestamp: yesterday, + }); + + // Add some events for today which must get aggregated + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: today, + }); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: today, + }); + + // Add new events, they will have today's timestamp. + await TrackingDBService.saveEvents(JSON.stringify(LOG)); + + // Ensure events that are inserted today are not aggregated with past events. + let rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKERS_ID, + }); + equal(rows.length, 2, "Tracker entries for today and yesterday, length is 2"); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 4, "Yesterday's count is 4"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.CRYPTOMINERS_ID, + }); + equal( + rows.length, + 2, + "Cryptominer entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 3, "Yesterday's count is 3"); + } else if (i == 1) { + equal(count, 3, "Today's count is 3, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.FINGERPRINTERS_ID, + }); + equal( + rows.length, + 2, + "Fingerprinter entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 2, "Yesterday's count is 2"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + rows = await db.execute(SQL.selectAllEntriesOfType, { + type: TrackingDBService.TRACKING_COOKIES_ID, + }); + equal( + rows.length, + 2, + "Tracking Cookies entries for today and yesterday, length is 2" + ); + for (let i = 0; i < rows.length; i++) { + let count = rows[i].getResultByName("count"); + if (i == 0) { + equal(count, 1, "Yesterday's count is 1"); + } else if (i == 1) { + equal(count, 5, "Today's count is 5, new entries were aggregated"); + } + } + + // Use the TrackingDBService API to delete the data. + await TrackingDBService.clearAll(); + // Make sure the data was deleted. + rows = await db.execute(SQL.selectAll); + equal(rows.length, 0, "length is 0"); + await db.close(); +}); + +let addEventsToDB = async db => { + let d = new Date(1521009000000); + let date = d.toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: 3, + timestamp: date, + }); + + date = new Date(d - 2 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKERS_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 3 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 4 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.TRACKING_COOKIES_ID, + count: 2, + timestamp: date, + }); + + date = new Date(d - 9 * 24 * 60 * 60 * 1000).toISOString(); + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.FINGERPRINTERS_ID, + count: 2, + timestamp: date, + }); +}; + +// This tests that TrackingDBService.getEventsByDateRange can accept two timestamps in unix epoch time +// and return entries that occur within the timestamps, rounded to the nearest day and inclusive. +add_task(async function test_getEventsByDateRange() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + await addEventsToDB(db); + + let d = new Date(1521009000000); + let daysBefore1 = new Date(d - 24 * 60 * 60 * 1000); + let daysBefore4 = new Date(d - 4 * 24 * 60 * 60 * 1000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000); + + let events = await TrackingDBService.getEventsByDateRange(daysBefore1, d); + equal( + events.length, + 1, + "There is 1 event entry between the date and one day before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange(daysBefore4, d); + equal( + events.length, + 4, + "There is 4 event entries between the date and four days before, inclusive" + ); + + events = await TrackingDBService.getEventsByDateRange( + daysBefore9, + daysBefore4 + ); + equal( + events.length, + 2, + "There is 2 event entries between nine and four days before, inclusive" + ); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.sumAllEvents returns the number of +// tracking events in the database, and can handle 0 entries. +add_task(async function test_sumAllEvents() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let sum = await TrackingDBService.sumAllEvents(); + equal(sum, 0, "There have been 0 events recorded"); + + // populate the database + await addEventsToDB(db); + + sum = await TrackingDBService.sumAllEvents(); + equal(sum, 11, "There have been 11 events recorded"); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that TrackingDBService.getEarliestRecordedDate returns the +// earliest date recorded and can handle 0 entries. +add_task(async function test_getEarliestRecordedDate() { + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + + let timestamp = await TrackingDBService.getEarliestRecordedDate(); + equal(timestamp, null, "There is no earliest recorded date"); + + // populate the database + await addEventsToDB(db); + let d = new Date(1521009000000); + let daysBefore9 = new Date(d - 9 * 24 * 60 * 60 * 1000) + .toISOString() + .split("T")[0]; + + timestamp = await TrackingDBService.getEarliestRecordedDate(); + let date = new Date(timestamp).toISOString().split("T")[0]; + equal(date, daysBefore9, "The earliest recorded event is nine days before."); + + await TrackingDBService.clearAll(); + await db.close(); +}); + +// This tests that a message to CFR is sent when the amount of saved trackers meets a milestone +add_task(async function test_sendMilestoneNotification() { + let milestones = JSON.parse( + Services.prefs.getStringPref( + "browser.contentblocking.cfr-milestone.milestones" + ) + ); + // This creates the schema. + await TrackingDBService.saveEvents(JSON.stringify({})); + let db = await Sqlite.openConnection({ path: DB_PATH }); + // save number of trackers equal to the first milestone + await db.execute(SQL.insertCustomTimeEvent, { + type: TrackingDBService.CRYPTOMINERS_ID, + count: milestones[0], + timestamp: new Date().toISOString(), + }); + + let awaitNotification = TestUtils.topicObserved( + "SiteProtection:ContentBlockingMilestone" + ); + + // trigger a "save" event to compare the trackers with the milestone. + await TrackingDBService.saveEvents( + JSON.stringify({ + "https://1.example.com": [ + [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1], + ], + }) + ); + await awaitNotification; + + await TrackingDBService.clearAll(); + await db.close(); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/test_view_source.js b/toolkit/components/antitracking/test/xpcshell/test_view_source.js new file mode 100644 index 0000000000..ebd70cf476 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/test_view_source.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); + +let gCookieHits = 0; +let gLoadingHits = 0; + +add_task(async function () { + do_get_profile(); + + info("Disable predictor and accept all"); + Services.prefs.setBoolPref("network.predictor.enabled", false); + Services.prefs.setBoolPref("network.predictor.enable-prefetch", false); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN + ); + + const server = CookieXPCShellUtils.createServer({ + hosts: ["example.org"], + }); + server.registerPathHandler("/test", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + if ( + request.hasHeader("Cookie") && + request.getHeader("Cookie") == "foo=bar" + ) { + gCookieHits++; + } else { + response.setHeader("Set-Cookie", "foo=bar"); + } + + gLoadingHits++; + var body = "<html></html>"; + response.bodyOutputStream.write(body, body.length); + }); + + info("Reset the hits count"); + gCookieHits = 0; + gLoadingHits = 0; + + info("Let's load a page"); + let contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info("Let's load the source of the page again to see if it loads from cache"); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?1" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 0, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 1, "The number of loading hits match"); + + info( + "Let's load the source of the page without hitting the cache to see if the cookie is sent properly" + ); + contentPage = await CookieXPCShellUtils.loadContentPage( + "view-source:http://example.org/test?2" + ); + await contentPage.close(); + + Assert.equal(gCookieHits, 1, "The number of cookie hits match"); + Assert.equal(gLoadingHits, 2, "The number of loading hits match"); +}); diff --git a/toolkit/components/antitracking/test/xpcshell/xpcshell.ini b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..b895aa8e07 --- /dev/null +++ b/toolkit/components/antitracking/test/xpcshell/xpcshell.ini @@ -0,0 +1,51 @@ +[DEFAULT] +head = head.js ../../../../components/url-classifier/tests/unit/head_urlclassifier.js +prefs = + dom.security.https_first=false #Disable https-first because of explicit http/https testing + +[test_cookie_behavior.js] +[test_getPartitionKeyFromURL.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_purge_trackers.js] +skip-if = + win10_2004 # Bug 1718292 + win10_2009 # Bug 1718292 + win11_2009 # Bug 1797751 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +run-sequentially = very high failure rate in parallel + +[test_purge_trackers_telemetry.js] +[test_tracking_db_service.js] +skip-if = toolkit == "android" # Bug 1697936 +[test_rejectForeignAllowList.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_clientAuthRemember.js] +[test_staticPartition_font.js] +support-files = + data/font.woff +skip-if = + os == "linux" && !debug # Bug 1760086 + apple_silicon # bug 1729551 + os == "mac" && bits == 64 && !debug # Bug 1652119 + os == "win" && bits == 64 && !debug # Bug 1652119 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + socketprocess_networking # Bug 1759035 +run-sequentially = very high failure rate in parallel +[test_staticPartition_image.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_authhttp.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_prefetch.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_staticPartition_preload.js] +skip-if = + socketprocess_networking # Bug 1759035 +[test_ExceptionListService.js] +[test_view_source.js] +skip-if = + socketprocess_networking # Bug 1759035 (not as common on win, perma on linux/osx) |