commit 3428dbfba2c87c0b8d486685c1294741e88f77d0
parent c3f7c6c25567350ffc5612d448ef62b734fb01e2
Author: jim <zijin@ualberta.ca>
Date: Mon, 13 Oct 2025 13:01:01 +0000
Bug 1886894 - add extension:purge-sessionStorage event to do exact domain match when domain is provided. r=robwu,dom-storage-reviewers,edenchuang
Differential Revision: https://phabricator.services.mozilla.com/D260525
Diffstat:
9 files changed, 80 insertions(+), 29 deletions(-)
diff --git a/dom/storage/LocalStorageManager.cpp b/dom/storage/LocalStorageManager.cpp
@@ -376,7 +376,8 @@ nsresult LocalStorageManager::Observe(const char* aTopic,
return NS_OK;
}
- if (!strcmp(aTopic, "browser:purge-sessionStorage")) {
+ if (!strcmp(aTopic, "browser:purge-sessionStorage") ||
+ !strcmp(aTopic, "extension:purge-sessionStorage")) {
// This is only meant for SessionStorageManager.
return NS_OK;
}
diff --git a/dom/storage/PBackgroundSessionStorageManager.ipdl b/dom/storage/PBackgroundSessionStorageManager.ipdl
@@ -23,7 +23,7 @@ sync protocol PBackgroundSessionStorageManager
parent:
async PBackgroundSessionStorageCache(PrincipalInfo aPrincipalInfo, nsCString aOriginKey);
- async ClearStorages(OriginAttributesPattern aPattern, nsCString aOriginScope);
+ async ClearStorages(OriginAttributesPattern aPattern, nsCString aOriginScope, uint32_t aMode);
async DeleteMe();
diff --git a/dom/storage/SessionStorageManager.cpp b/dom/storage/SessionStorageManager.cpp
@@ -34,6 +34,21 @@ namespace mozilla::dom {
using namespace StorageUtils;
+static bool ExactDomainMatch(const nsACString& aOriginKey,
+ const nsACString& aOriginScope) {
+ // aOriginScope is reversed domain with trailing dot.
+ // e.g: example.com => moc.elpmaxe. (see StorageUtils.cpp)
+ // aOriginKey is reversed domain with trailing dot, plus ":",
+ // scheme, and optional port. e.g: moc.elpmaxe.:http:80 (see
+ // https://searchfox.org/firefox-main/rev/987f566373ea82403c5c1235b219bd9e7d56a4aa/caps/BasePrincipal.cpp#1539)
+ // To check if it is an exact match, we need to ensure that aOriginKey starts
+ // with aOriginScope and the first character immediately following the
+ // matching part is ":". i.e: Domain part of aOriginKey is identical to
+ // aOriginScope.
+ return StringBeginsWith(aOriginKey, aOriginScope) &&
+ aOriginKey.CharAt(aOriginScope.Length()) == ':';
+}
+
// Parent process, background thread hashmap that stores top context id and
// manager pair.
static StaticAutoPtr<
@@ -151,7 +166,10 @@ bool RecvClearStoragesForOrigin(const nsACString& aOriginAttrs,
}
void SessionStorageManagerBase::ClearStoragesInternal(
- const OriginAttributesPattern& aPattern, const nsACString& aOriginScope) {
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode) {
+ const bool isExactMatch = aMode == DomainMatchingMode::EXACT_MATCH;
+
for (const auto& oaEntry : mOATable) {
OriginAttributes oa;
DebugOnly<bool> ok = oa.PopulateFromSuffix(oaEntry.GetKey());
@@ -164,7 +182,10 @@ void SessionStorageManagerBase::ClearStoragesInternal(
OriginKeyHashTable* table = oaEntry.GetWeak();
for (const auto& originKeyEntry : *table) {
if (aOriginScope.IsEmpty() ||
- StringBeginsWith(originKeyEntry.GetKey(), aOriginScope)) {
+ (!isExactMatch &&
+ StringBeginsWith(originKeyEntry.GetKey(), aOriginScope)) ||
+ (isExactMatch &&
+ ExactDomainMatch(originKeyEntry.GetKey(), aOriginScope))) {
const auto cache = originKeyEntry.GetData()->mCache;
cache->Clear(false);
cache->ResetWriteInfos();
@@ -626,17 +647,19 @@ SessionStorageManager::CheckStorage(nsIPrincipal* aPrincipal, Storage* aStorage,
}
void SessionStorageManager::ClearStorages(
- const OriginAttributesPattern& aPattern, const nsACString& aOriginScope) {
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode) {
if (CanLoadData()) {
nsresult rv = EnsureManager();
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
- mActor->SendClearStorages(aPattern, nsCString(aOriginScope));
+ mActor->SendClearStorages(aPattern, nsCString(aOriginScope),
+ static_cast<uint32_t>(aMode));
}
- ClearStoragesInternal(aPattern, aOriginScope);
+ ClearStoragesInternal(aPattern, aOriginScope, aMode);
}
nsresult SessionStorageManager::Observe(
@@ -668,6 +691,13 @@ nsresult SessionStorageManager::Observe(
return NS_OK;
}
+ // Clear everything (including so and pb data) from caches and database
+ // for the given domain.
+ if (!strcmp(aTopic, "extension:purge-sessionStorage")) {
+ ClearStorages(pattern, aOriginScope, DomainMatchingMode::EXACT_MATCH);
+ return NS_OK;
+ }
+
// Clear entries which match an OriginAttributesPattern.
if (!strcmp(aTopic, "dom-storage:clear-origin-attributes-data") ||
!strcmp(aTopic, "session-storage:clear-origin-attributes-data")) {
@@ -893,10 +923,11 @@ void BackgroundSessionStorageManager::UpdateData(
}
void BackgroundSessionStorageManager::ClearStorages(
- const OriginAttributesPattern& aPattern, const nsACString& aOriginScope) {
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode) {
MOZ_ASSERT(XRE_IsParentProcess());
::mozilla::ipc::AssertIsOnBackgroundThread();
- ClearStoragesInternal(aPattern, aOriginScope);
+ ClearStoragesInternal(aPattern, aOriginScope, aMode);
}
void BackgroundSessionStorageManager::ClearStoragesForOrigin(
diff --git a/dom/storage/SessionStorageManager.h b/dom/storage/SessionStorageManager.h
@@ -43,6 +43,16 @@ bool RecvLoadSessionStorageData(
bool RecvClearStoragesForOrigin(const nsACString& aOriginAttrs,
const nsACString& aOriginKey);
+// DomainMatchingMode is used to allow ClearStorages to mimic one of
+// the LSNG behaviours - exact domain match.
+// When ClearStorages is invoked with EXACT_MATCH, it clears only the
+// data for a given domain, and would not affect any subdomains.
+// Currently EXACT_MATCH is only passed when browser:purge-sessionStorage event
+// is triggered. By default, ClearStorages is called with PREFIX_MATCH, which
+// clears data for a given domain and all the subdomains. This ensures that the
+// behaviour of other events that call ClearStorages remain unchanged.
+enum class DomainMatchingMode { PREFIX_MATCH, EXACT_MATCH };
+
class BrowsingContext;
class ContentParent;
class SSSetItemInfo;
@@ -90,8 +100,9 @@ class SessionStorageManagerBase {
FlippedOnce<false> mLoaded;
};
- void ClearStoragesInternal(const OriginAttributesPattern& aPattern,
- const nsACString& aOriginScope);
+ void ClearStoragesInternal(
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);
void ClearStoragesForOriginInternal(const nsACString& aOriginAttrs,
const nsACString& aOriginKey);
@@ -153,8 +164,9 @@ class SessionStorageManager final : public SessionStorageManagerBase,
SessionStorageCache* aCloneFrom,
RefPtr<SessionStorageCache>* aRetVal);
- void ClearStorages(const OriginAttributesPattern& aPattern,
- const nsACString& aOriginScope);
+ void ClearStorages(
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);
SessionStorageCacheChild* EnsureCache(nsIPrincipal& aPrincipal,
const nsACString& aOriginKey,
@@ -213,8 +225,9 @@ class BackgroundSessionStorageManager final : public SessionStorageManagerBase {
void UpdateData(const nsACString& aOriginAttrs, const nsACString& aOriginKey,
const nsTArray<SSSetItemInfo>& aData);
- void ClearStorages(const OriginAttributesPattern& aPattern,
- const nsACString& aOriginScope);
+ void ClearStorages(
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ DomainMatchingMode aMode = DomainMatchingMode::PREFIX_MATCH);
void ClearStoragesForOrigin(const nsACString& aOriginAttrs,
const nsACString& aOriginKey);
diff --git a/dom/storage/StorageIPC.cpp b/dom/storage/StorageIPC.cpp
@@ -1452,9 +1452,11 @@ BackgroundSessionStorageManager* SessionStorageManagerParent::GetManager()
}
mozilla::ipc::IPCResult SessionStorageManagerParent::RecvClearStorages(
- const OriginAttributesPattern& aPattern, const nsACString& aOriginScope) {
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ const uint32_t& aMode) {
::mozilla::ipc::AssertIsOnBackgroundThread();
- mBackgroundManager->ClearStorages(aPattern, aOriginScope);
+ mBackgroundManager->ClearStorages(aPattern, aOriginScope,
+ static_cast<DomainMatchingMode>(aMode));
return IPC_OK();
}
diff --git a/dom/storage/StorageIPC.h b/dom/storage/StorageIPC.h
@@ -561,8 +561,8 @@ class SessionStorageManagerParent final
BackgroundSessionStorageManager* GetManager() const;
mozilla::ipc::IPCResult RecvClearStorages(
- const OriginAttributesPattern& aPattern,
- const nsACString& aOriginScope) override;
+ const OriginAttributesPattern& aPattern, const nsACString& aOriginScope,
+ const uint32_t& aMode) override;
private:
~SessionStorageManagerParent();
diff --git a/dom/storage/StorageObserver.cpp b/dom/storage/StorageObserver.cpp
@@ -71,6 +71,7 @@ nsresult StorageObserver::Init() {
obs->AddObserver(sSelf, "dom-storage:clear-origin-attributes-data", true);
obs->AddObserver(sSelf, "extension:purge-localStorage", true);
obs->AddObserver(sSelf, "browser:purge-sessionStorage", true);
+ obs->AddObserver(sSelf, "extension:purge-sessionStorage", true);
// Shutdown
obs->AddObserver(sSelf, "profile-after-change", true);
@@ -358,7 +359,8 @@ StorageObserver::Observe(nsISupports* aSubject, const char* aTopic,
return NS_OK;
}
- if (!strcmp(aTopic, "browser:purge-sessionStorage")) {
+ if (!strcmp(aTopic, "browser:purge-sessionStorage") ||
+ !strcmp(aTopic, "extension:purge-sessionStorage")) {
// The caller passed an nsIClearBySiteEntry object which consists of both
// site and pattern.
// If both are passed, aSubject takes precedence over aData.
diff --git a/toolkit/components/extensions/parent/ext-browsingData.js b/toolkit/components/extensions/parent/ext-browsingData.js
@@ -223,9 +223,9 @@ const clearLocalStorage = async function (options) {
)
: "";
- Services.obs.notifyObservers(entry, "browser:purge-sessionStorage");
+ Services.obs.notifyObservers(entry, "extension:purge-sessionStorage");
} else {
- Services.obs.notifyObservers(null, "browser:purge-sessionStorage");
+ Services.obs.notifyObservers(null, "extension:purge-sessionStorage");
}
};
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_browsingData_sessionStorage.html b/toolkit/components/extensions/test/mochitest/test_ext_browsingData_sessionStorage.html
@@ -65,8 +65,7 @@ add_task(async function testLocalStorage() {
});
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 browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkSessionStorageSet");
await sendMessageToTabs(tabs, "resetSessionStorage");
@@ -93,7 +92,7 @@ add_task(async function testLocalStorage() {
// TODO: containers support is lacking on GeckoView (Bug 1643740)
if (!navigator.userAgent.includes("Android")) {
- await browser.tabs.sendMessage(tabs["https://test1.example.org/"].id, "checkSessionStorageCleared");
+ await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkSessionStorageCleared");
}
await sendMessageToTabs(tabs, "resetSessionStorage");
@@ -102,13 +101,16 @@ add_task(async function testLocalStorage() {
// Happy path: Passing both hostname and cookieStoreId would clear the sessionStorage.
await browser.browsingData.removeLocalStorage({
cookieStoreId: "firefox-container-1",
- hostnames: ["example.org"],
+ hostnames: ["test1.example.com"],
});
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");
+ // TODO: containers support is lacking on GeckoView (Bug 1643740)
+ if (!navigator.userAgent.includes("Android")) {
+ await browser.tabs.sendMessage(tabs["https://test1.example.com/"].id, "checkSessionStorageCleared");
+ }
await sendMessageToTabs(tabs, "resetSessionStorage");
await sendMessageToTabs(tabs, "checkSessionStorageSet");
// Hostname doesn't match, so nothing cleared.
@@ -177,7 +179,7 @@ add_task(async function testLocalStorage() {
{ url: "https://example.com" },
{ url: "https://example.net" },
{
- url: "https://test1.example.org",
+ url: "https://test1.example.com",
cookieStoreId: 'firefox-container-1',
},
];
@@ -234,7 +236,7 @@ add_task(async function testLocalStorage() {
matches: [
"https://example.com/",
"https://example.net/",
- "https://test1.example.org/",
+ "https://test1.example.com/",
],
js: ["content-script.js"],
run_at: "document_end",