commit a315c687a53916f96d862e83021db8543395b2cc
parent d904c4b2823340b08533984f19f9189b9bba6ee0
Author: jim <zijin@ualberta.ca>
Date: Mon, 6 Oct 2025 21:48:52 +0000
Bug 1886894 - removeLocalStorage now removes both localStorage and sessionStorage, a unit test is added. r=robwu
Differential Revision: https://phabricator.services.mozilla.com/D256723
Diffstat:
3 files changed, 287 insertions(+), 0 deletions(-)
diff --git a/toolkit/components/extensions/parent/ext-browsingData.js b/toolkit/components/extensions/parent/ext-browsingData.js
@@ -210,6 +210,27 @@ const clearLocalStorage = async function (options) {
message: "Firefox does not support clearing localStorage with 'since'.",
});
}
+ const notifySessionStorage = function (hostname, cookieStoreId) {
+ if (hostname || cookieStoreId) {
+ const entry = Cc["@mozilla.org/clear-by-site-entry;1"].createInstance(
+ Ci.nsIClearBySiteEntry
+ );
+
+ //TODO: currently, passing cookieStoreId with empty hostname is not supported because
+ // CreateReversedDomain will reject empty string.
+ entry.schemelessSite = hostname || "";
+
+ entry.patternJSON = cookieStoreId
+ ? JSON.stringify(
+ getOriginAttributesPatternForCookieStoreId(cookieStoreId)
+ )
+ : "";
+
+ Services.obs.notifyObservers(entry, "browser:purge-sessionStorage");
+ } else {
+ Services.obs.notifyObservers(null, "browser:purge-sessionStorage");
+ }
+ };
// The legacy LocalStorage implementation that will eventually be removed
// depends on this observer notification. Some other subsystems like
@@ -222,9 +243,12 @@ const clearLocalStorage = async function (options) {
"extension:purge-localStorage",
hostname
);
+
+ notifySessionStorage(hostname, options.cookieStoreId);
}
} else {
Services.obs.notifyObservers(null, "extension:purge-localStorage");
+ notifySessionStorage(null, options.cookieStoreId);
}
if (Services.domStorageManager.nextGenLocalStorageEnabled) {
diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.toml b/toolkit/components/extensions/test/mochitest/mochitest-common.toml
@@ -159,6 +159,8 @@ skip-if = [
"http2",
]
+["test_ext_browsingData_sessionStorage.html"]
+
["test_ext_browsingData_settings.html"]
["test_ext_canvas_resistFingerprinting.html"]
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_sessionStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_sessionStorage.html
@@ -0,0 +1,261 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test browsingData.removeLocalStorage also removes sessionStorage</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function setup() {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+});
+
+add_task(async function testLocalStorage() {
+ async function background() {
+ function waitForTabs() {
+ return new Promise(resolve => {
+ let tabs = {};
+
+ let listener = async (msg, { tab }) => {
+ if (msg !== "content-script-ready") {
+ return;
+ }
+
+ tabs[tab.url] = tab;
+ if (Object.keys(tabs).length == 3) {
+ browser.runtime.onMessage.removeListener(listener);
+ resolve(tabs);
+ }
+ };
+ browser.runtime.onMessage.addListener(listener);
+ });
+ }
+
+ function sendMessageToTabs(tabs, message) {
+ return Promise.all(
+ Object.values(tabs).map(tab => {
+ return browser.tabs.sendMessage(tab.id, message);
+ })
+ );
+ }
+
+ let tabs = await waitForTabs();
+
+ browser.test.assertRejects(
+ browser.browsingData.removeLocalStorage({ since: Date.now() }),
+ "Firefox does not support clearing localStorage with 'since'.",
+ "Expected error received when using unimplemented parameter 'since'."
+ );
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+
+ await browser.browsingData.removeLocalStorage({
+ hostnames: ["example.com"],
+ });
+ await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkSessionStorageCleared");
+ await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkSessionStorageSet");
+ ////TODO: Legacy sessionStorage implementation would not recognize the difference between example.com and test1.example.com, temporarily use example.org instead.
+ await browser.tabs.sendMessage(tabs["https://test1.example.org/"].id, "checkSessionStorageSet");
+
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ await browser.browsingData.removeLocalStorage({});
+ await sendMessageToTabs(tabs, "checkSessionStorageCleared");
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ await browser.browsingData.remove({}, { localStorage: true });
+ await sendMessageToTabs(tabs, "checkSessionStorageCleared");
+
+
+ // Can only delete cookieStoreId with LSNG enabled.
+ if (SpecialPowers.Services.domStorageManager.nextGenLocalStorageEnabled) {
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ // TODO: passing only the cookieStoreId is not supported, it will not delete anything.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ });
+ await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkSessionStorageSet");
+ await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkSessionStorageSet");
+
+ // TODO: containers support is lacking on GeckoView (Bug 1643740)
+ if (!navigator.userAgent.includes("Android")) {
+ await browser.tabs.sendMessage(tabs["https://test1.example.org/"].id, "checkSessionStorageSet");
+ }
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+
+ // Happy path: Passing both hostname and cookieStoreId would clear the sessionStorage.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ hostnames: ["example.org"],
+ });
+
+ await browser.tabs.sendMessage(tabs["https://example.com/"].id, "checkSessionStorageSet");
+ await browser.tabs.sendMessage(tabs["https://example.net/"].id, "checkSessionStorageSet");
+ await browser.tabs.sendMessage(tabs["https://test1.example.org/"].id, "checkSessionStorageCleared");
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ // Hostname doesn't match, so nothing cleared.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ hostnames: ["example.net"],
+ });
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+
+ await sendMessageToTabs(tabs, "resetSessionStorage");
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ // Deleting private browsing mode data is silently ignored.
+ await browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-private",
+ });
+ await sendMessageToTabs(tabs, "checkSessionStorageSet");
+ } else {
+ await browser.test.assertRejects(
+ browser.browsingData.removeLocalStorage({
+ cookieStoreId: "firefox-container-1",
+ }),
+ "Firefox does not support clearing localStorage with 'cookieStoreId'.",
+ "removeLocalStorage with cookieStoreId requires LSNG"
+ );
+ }
+
+ await browser.browsingData.removeLocalStorage({});
+
+ browser.test.notifyPass("done");
+ }
+
+ function contentScript() {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg === "resetSessionStorage") {
+ sessionStorage.clear();
+ sessionStorage.setItem("key", "value");
+ } else if (msg === "checkSessionStorageSet") {
+ browser.test.assertEq(
+ "value",
+ sessionStorage.getItem("key"),
+ `checkSessionStorageSet: ${location.href}`
+ );
+ } else if (msg === "checkSessionStorageCleared") {
+ browser.test.assertEq(
+ null,
+ sessionStorage.getItem("key"),
+ `checkSessionStorageCleared: ${location.href}`
+ );
+ }
+ });
+ browser.runtime.sendMessage("content-script-ready");
+ }
+
+ // This extension is responsible for opening tabs with a specified
+ // cookieStoreId, we use a separate extension to make sure that browsingData
+ // works without the cookies permission.
+ let openTabsExtension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "Open tabs",
+ browser_specific_settings: { gecko: { id: "open-tabs@tests.mozilla.org" }, },
+ permissions: ["cookies"],
+ },
+ async background() {
+ const TABS = [
+ { url: "https://example.com" },
+ { url: "https://example.net" },
+ {
+ url: "https://test1.example.org",
+ cookieStoreId: 'firefox-container-1',
+ },
+ ];
+
+ function awaitLoad(tabId) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(tabId_, changed) {
+ if (tabId == tabId_ && changed.status == "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ }
+
+ let tabs = [];
+ let loaded = [];
+ for (let options of TABS) {
+ let tab = await browser.tabs.create(options);
+ loaded.push(awaitLoad(tab.id));
+ tabs.push(tab);
+ }
+
+ await Promise.all(loaded);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "cleanup") {
+ const tabIds = tabs.map(tab => tab.id);
+ let removedTabs = 0;
+ browser.tabs.onRemoved.addListener(tabId => {
+ browser.test.log(`Removing tab ${tabId}.`);
+ if (tabIds.includes(tabId)) {
+ removedTabs++;
+ if (removedTabs == tabIds.length) {
+ browser.test.sendMessage("done");
+ }
+ }
+ });
+ await browser.tabs.remove(tabIds);
+ }
+ });
+ }
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ background,
+ manifest: {
+ name: "Test Extension",
+ browser_specific_settings: { gecko: { id: "localStorage@tests.mozilla.org" } },
+ permissions: ["browsingData", "tabs"],
+ content_scripts: [
+ {
+ matches: [
+ "https://example.com/",
+ "https://example.net/",
+ "https://test1.example.org/",
+ ],
+ js: ["content-script.js"],
+ run_at: "document_end",
+ },
+ ],
+ },
+ files: {
+ "content-script.js": contentScript,
+ },
+ });
+
+ await openTabsExtension.startup();
+
+ await extension.startup();
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ await openTabsExtension.sendMessage("cleanup");
+ await openTabsExtension.awaitMessage("done");
+ await openTabsExtension.unload();
+});
+</script>
+</body>
+</html>