Bug 1406675 - Allow storages in WebExtensions on customized cookieBehavior and lifetimePolicy prefs. r=aswan,asuth
This commit ensures that WebExtension principals always get a nsICookieService::BEHAVIOR_ACCEPT
cookieBehavior and a nsICookieService::ACCEPT_NORMALLY aLifetimePolicy:
- the webextension pages are still able to use indexedDB and localStorage on a globally
configured:
"network.cookie.cookieBehavior = 2"
("Accept cookies from websites" unchecked in the about:preferences
"use custom settings for history" section)
- the webextension pages' localStorage does not switch in session-only mode on a globally
configured:
"network.cookie.lifetimePolicy = 2"
("Keep until I close Firefox" in the about:preferences
"use custom settings for history" section)
MozReview-Commit-ID: 5LOCvCgcokM
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -9133,17 +9133,27 @@ nsContentUtils::InternalStorageAllowedFo
// Check if we are in private browsing, and record that fact
if (IsInPrivateBrowsing(document)) {
access = StorageAccess::ePrivateBrowsing;
}
}
uint32_t lifetimePolicy;
uint32_t behavior;
- GetCookieBehaviorForPrincipal(aPrincipal, &lifetimePolicy, &behavior);
+
+ // WebExtensions principals always get BEHAVIOR_ACCEPT as cookieBehavior
+ // and ACCEPT_NORMALLY as lifetimePolicy (See Bug 1406675 for rationale).
+ auto policy = BasePrincipal::Cast(aPrincipal)->AddonPolicy();
+
+ if (policy) {
+ behavior = nsICookieService::BEHAVIOR_ACCEPT;
+ lifetimePolicy = nsICookieService::ACCEPT_NORMALLY;
+ } else {
+ GetCookieBehaviorForPrincipal(aPrincipal, &lifetimePolicy, &behavior);
+ }
// Check if we should only allow storage for the session, and record that fact
if (lifetimePolicy == nsICookieService::ACCEPT_SESSION) {
// Storage could be StorageAccess::ePrivateBrowsing or StorageAccess::eAllow
// so perform a std::min comparison to make sure we preserve ePrivateBrowsing
// if it has been set.
access = std::min(StorageAccess::eSessionScoped, access);
}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_cookieBehaviors.js
@@ -0,0 +1,216 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
+const {
+ // cookieBehavior constants.
+ BEHAVIOR_REJECT_FOREIGN,
+ BEHAVIOR_REJECT,
+ BEHAVIOR_LIMIT_FOREIGN,
+
+ // lifetimePolicy constants.
+ ACCEPT_SESSION,
+} = Ci.nsICookieService;
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
+
+// Test that the indexedDB and localStorage are allowed in an extension background page
+// and that the indexedDB is allowed in a extension worker.
+async function test_bg_page_allowed_storage() {
+ function background() {
+ try {
+ void indexedDB;
+ void localStorage;
+
+ const worker = new Worker("worker.js");
+ worker.onmessage = (event) => {
+ if (event.data.pass) {
+ browser.test.notifyPass("bg_allowed_storage");
+ } else {
+ browser.test.notifyFail("bg_allowed_storage");
+ }
+ };
+
+ worker.postMessage({});
+ } catch (err) {
+ browser.test.notifyFail("bg_allowed_storage");
+ throw err;
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ files: {
+ "worker.js": function worker() {
+ this.onmessage = () => {
+ try {
+ void indexedDB;
+ postMessage({pass: true});
+ } catch (err) {
+ postMessage({pass: false});
+ throw err;
+ }
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("bg_allowed_storage");
+ await extension.unload();
+}
+
+add_task(async function test_ext_page_allowed_storage_on_cookieBehaviors() {
+ do_print("Test background page indexedDB with BEHAVIOR_LIMIT_FOREIGN");
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", BEHAVIOR_LIMIT_FOREIGN);
+ await test_bg_page_allowed_storage();
+
+ do_print("Test background page indexedDB with BEHAVIOR_REJECT_FOREIGN");
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", BEHAVIOR_REJECT_FOREIGN);
+ await test_bg_page_allowed_storage();
+
+ do_print("Test background page indexedDB with BEHAVIOR_REJECT");
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", BEHAVIOR_REJECT);
+ await test_bg_page_allowed_storage();
+});
+
+// Test that the webpage's indexedDB and localStorage are still not allowed from a content script
+// when the cookie behavior, even when they are allowed in the extension pages.
+add_task(async function test_content_script_on_cookieBehaviorReject() {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", BEHAVIOR_REJECT);
+
+ function contentScript() {
+ // Ensure that when the current cookieBehavior doesn't allow a webpage to use indexedDB
+ // or localStorage, then a WebExtension content script is not allowed to use it as well.
+ browser.test.assertThrows(
+ () => indexedDB,
+ /The operation is insecure/,
+ "a content script can't use indexedDB from a page where it is disallowed"
+ );
+
+ browser.test.assertThrows(
+ () => localStorage,
+ /The operation is insecure/,
+ "a content script can't use localStorage from a page where it is disallowed"
+ );
+
+ browser.test.notifyPass("cs_disallowed_storage");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [{
+ matches: ["http://*/*/file_sample.html"],
+ js: ["content_script.js"],
+ }],
+ },
+ files: {
+ "content_script.js": contentScript,
+ },
+ });
+
+ await extension.startup();
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(`${BASE_URL}/file_sample.html`);
+
+ await extension.awaitFinish("cs_disallowed_storage");
+
+ await contentPage.close();
+ await extension.unload();
+});
+
+add_task(function clear_cookieBehavior_pref() {
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+});
+
+// Test that localStorage is not in session-only mode for the extension pages,
+// even when the session-only mode has been globally enabled.
+add_task(async function test_localStorage_on_session_lifetimePolicy() {
+ // localStorage in session-only mode.
+ Services.prefs.setIntPref("network.cookie.lifetimePolicy", ACCEPT_SESSION);
+
+ function background() {
+ localStorage.setItem("test-key", "test-value");
+
+ browser.test.sendMessage("bg_localStorage_set", {uuid: window.location.hostname});
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ });
+
+ await extension.startup();
+ const {uuid} = await extension.awaitMessage("bg_localStorage_set");
+
+ const fakeAddonActor = {addonId: extension.id};
+ const addonBrowser = await ExtensionParent.DebugUtils.getExtensionProcessBrowser(fakeAddonActor);
+ const {isRemoteBrowser} = addonBrowser;
+
+ const {
+ isSessionOnly,
+ domStorageLength,
+ domStorageStoredValue,
+ } = await ContentTask.spawn(addonBrowser, {uuid, isRemoteBrowser}, (params) => {
+ const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+ let windowEnumerator = Services.ww.getWindowEnumerator();
+
+ let bgPageWindow;
+
+ // Search the background page window in the process where the extension is running.
+ while (windowEnumerator.hasMoreElements()) {
+ let win = windowEnumerator.getNext();
+
+ // When running in remote-webextension mode the window enumerator
+ // will only include top level windows related to the extension process
+ // (the background page and the "about:blank" related to the addonBrowser
+ // used to connect to the right process).
+
+ if (!params.isRemoteBrowser) {
+ if (win.location.href !== "chrome://extensions/content/dummy.xul") {
+ // When running in single process mode, all the top level windows
+ // will be enumerated, we ignore any window that is not an
+ // extension windowlessBrowser.
+ continue;
+ } else {
+ // from an extension windowlessBrowser, retrieve the background page window
+ // (which has been loaded inside a XUL browser element).
+ win = win.document.querySelector("browser").contentWindow;
+ }
+ }
+
+ if (win.location.hostname === params.uuid &&
+ win.location.pathname === "/_generated_background_page.html") {
+ // Once we have found the background page window related to the target extension
+ // we can exit the while loop.
+ bgPageWindow = win;
+ break;
+ }
+ }
+
+ if (!bgPageWindow) {
+ throw new Error("Unable to find the extension background page");
+ }
+
+ return {
+ isSessionOnly: bgPageWindow.localStorage.isSessionOnly,
+ domStorageLength: bgPageWindow.localStorage.length,
+ domStorageStoredValue: bgPageWindow.localStorage.getItem("test-key"),
+ };
+ });
+
+ await ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(fakeAddonActor);
+
+ equal(isSessionOnly, false, "the extension localStorage is not set in session-only mode");
+ equal(domStorageLength, 1, "the extension storage contains the expected number of keys");
+ equal(domStorageStoredValue, "test-value", "the extension storage contains the expected data");
+
+ await extension.unload();
+});
+
+add_task(function clear_lifetimePolicy_pref() {
+ Services.prefs.clearUserPref("network.cookie.lifetimePolicy");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -11,16 +11,17 @@ skip-if = os == "android" # Android does
[test_ext_background_runtime_connect_params.js]
[test_ext_background_sub_windows.js]
[test_ext_background_telemetry.js]
[test_ext_background_window_properties.js]
skip-if = os == "android"
[test_ext_browserSettings.js]
[test_ext_browserSettings_homepage.js]
skip-if = os == "android"
+[test_ext_cookieBehaviors.js]
[test_ext_contextual_identities.js]
skip-if = os == "android" # Containers are not exposed to android.
[test_ext_debugging_utils.js]
[test_ext_downloads.js]
[test_ext_downloads_download.js]
skip-if = os == "android"
[test_ext_downloads_misc.js]
skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870