/* 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, iframeAllow = null ) { // Normal mode this.runTest( name, callbackTracking, callbackNonTracking, cleanupFunction, extraPrefs, windowOpenTest, userInteractionTest, expectedBlockingNotifications, false, iframeSandbox, accessRemoval, callbackAfterRemoval, iframeAllow ); // Private mode this.runTest( name, callbackTracking, callbackNonTracking, cleanupFunction, extraPrefs, windowOpenTest, userInteractionTest, expectedBlockingNotifications, true, iframeSandbox, accessRemoval, callbackAfterRemoval, iframeAllow ); }, 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, iframeAllow = 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, iframeAllow, }); 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, iframeAllow, }); 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, iframeAllow, }); 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, iframeAllow, }); 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, iframeAllow, }); this._createCleanupTask(cleanupFunction); this._createTask({ name: name + " reject foreign with exception", cookieBehavior: BEHAVIOR_REJECT_FOREIGN, allowList: true, callback: callbackNonTracking, extraPrefs: [...(extraPrefs || [])], expectedBlockingNotifications: 0, runInPrivateWindow, iframeSandbox, accessRemoval, callbackAfterRemoval, iframeAllow, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_FOREIGN, allowList: false, callback: callbackTracking, extraPrefs: [...(extraPrefs || [])], expectedBlockingNotifications: expectedBlockingNotifications ? Ci.nsIWebProgressListener.STATE_COOKIES_BLOCKED_FOREIGN : 0, runInPrivateWindow, iframeSandbox, accessRemoval, callbackAfterRemoval, iframeAllow, }); this._createCleanupTask(cleanupFunction); this._createTask({ name, cookieBehavior: BEHAVIOR_REJECT_TRACKER, allowList: true, callback: callbackNonTracking, extraPrefs, expectedBlockingNotifications: 0, runInPrivateWindow, iframeSandbox, accessRemoval, callbackAfterRemoval, iframeAllow, }); 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, iframeAllow, }); 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, iframeAllow, }); 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, iframeAllow ); this._createCleanupTask(cleanupFunction); // Now, check if it works for nested iframes. this._createWindowOpenTask( name, BEHAVIOR_REJECT_TRACKER, callbackTracking, callbackNonTracking, runInPrivateWindow, iframeSandbox, true, extraPrefs, iframeAllow ); 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, iframeAllow ); this._createCleanupTask(cleanupFunction); // Now, check if it works for nested iframes. this._createUserInteractionTask( name, BEHAVIOR_REJECT_TRACKER, callbackTracking, callbackNonTracking, runInPrivateWindow, iframeSandbox, true, extraPrefs, iframeAllow ); this._createCleanupTask(cleanupFunction); } } }, _waitObserver(targetTopic, expectedCount) { let cnt = 0; return new Promise(resolve => { Services.obs.addObserver(function observer(subject, topic) { 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 iframe allow set to " + options.iframeAllow + " 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, iframeAllow: options.iframeAllow, 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } 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.startLoadingURIString(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, iframeAllow: options.iframeAllow, }, ], 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } 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, iframeAllow ) { 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, iframeAllow, }, ], 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } 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, iframeAllow ) { 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, iframeAllow, }, ], 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } 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 ) { 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, iframeAllow, }, ], 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); } if (typeof obj.iframeAllow == "string") { ifr.setAttribute("allow", obj.iframeAllow); } content.document.body.appendChild(ifr); ifr.src = obj.page; await loading; let windowClosed = new content.Promise(resolve => { Services.ww.registerNotification(function notification( aSubject, aTopic ) { 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() {}, onDataAvailable(request, stream, off, cnt) { // Consume the data to prevent hitting the assertion. NetUtil.readInputStreamToString(stream, cnt); }, onStopRequest(request) { 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 ); }, };