Medium firefox Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionInformation disclosure in the DOM: Push Subscriptions component
ComponentDOM
Bug ClassInfo Disclosure
Tracker2059053
Fix commit64633df4508e (firefox) +10/-11
CISA KEVNot listed
CreditedKagami Rosylight
Disclosed2026-08-18

Background

Push Subscriptions
A DOM API (PushManager) that lets a service worker register with a push service and later retrieve its existing subscription via getSubscription().
Effective storage principal
The principal Gecko uses for storage-scoped decisions, carrying OriginAttributes including a partition key that reflects the top-level site when content is embedded third-party.
Partition key
A field in OriginAttributes (mPartitionKey) that is empty for first-party/unpartitioned contexts and non-empty when the origin is loaded under a different top-level site.
Permission state
The push notification permission (PermissionState::Granted/Denied) queried per-principal to decide whether a push subscription may be exposed.

Root Cause Analysis

The vulnerable code path is GetSubscriptionRunnable::Run in dom/push/PushManager.cpp, which handles a service worker’s request to fetch its existing push subscription. It called GetPermissionState(principal, state) using only the worker’s plain principal, without consulting the effective storage principal or its partition key, so a partitioned (third-party embedded) service worker was treated identically to a first-party one. The violated invariant is that a push subscription tied to a first-party origin must not be disclosed to that same origin when it is embedded as a partitioned third party, since storage/state should be isolated per partition.

The fix retrieves GetEffectiveStoragePrincipal() and only performs the permission check (and thus only allows a subscription to be returned) when mPartitionKey.IsEmpty(); otherwise state remains initialized to PermissionState::Denied. Because a non-granted state short-circuits the runnable before any subscription is looked up or returned, partitioned contexts can no longer read the unpartitioned subscription. The removed WPT expectations for cross-origin-nested confirm the fix restores correct cross-origin/nested behavior.

Key insight
The core mistake was performing the push permission and subscription lookup against the bare principal while ignoring storage partitioning, letting a third-party partitioned context reach first-party push state. The fix gates the lookup on an empty partition key of the effective storage principal, so only genuinely first-party contexts can obtain the subscription.

Attack Path

  1. Embed victim origin An attacker page embeds the victim origin (which has an active first-party push subscription) in a nested/third-party context under the attacker’s top-level site.
  2. Run a service worker The embedded partitioned context registers or reuses a service worker for the victim origin.
  3. Call getSubscription The worker invokes getSubscription(), driving GetSubscriptionRunnable::Run on the main thread.
  4. Bypass partitioning The permission check runs against the unpartitioned principal, returning Granted, so the first-party subscription is fetched and handed back to the partitioned context.
  5. Exfiltrate subscription The attacker-controlled embed reads the disclosed subscription details, leaking cross-partition state.

Impact Assessment

An attacker gains read access to a victim origin’s existing push subscription (information disclosure) from a partitioned third-party context that should be storage-isolated, running in content/DOM scope within a service worker. Preconditions are that the victim origin has an active push subscription and can be embedded by the attacker as a nested/third-party frame that instantiates its service worker. The impact is rated moderate, limited to disclosure rather than code execution.

Changed Functions

FunctionChangeNotes
if
dom/push/PushManager.cpp
modified

Files Changed

  • dom/push/PushManager.cpp
  • testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini

Audit Directions

  • Principal vs. storage principal
    Audit any state or permission check that uses a bare principal where a partition-aware GetEffectiveStoragePrincipal() should govern storage-scoped data access.
  • Partition key gating
    Look for storage/permission lookups that fail to inspect OriginAttributesRef().mPartitionKey, which can let third-party partitioned contexts read first-party state.
  • Default-deny initialization
    Verify that permission/state variables are initialized to a denying default (e.g., PermissionState::Denied) so a skipped or short-circuited check cannot fall through to a granting path.
diff --git a/dom/push/PushManager.cpp b/dom/push/PushManager.cpp
index 3deff5ec077..caba571b0c1 100644
--- a/dom/push/PushManager.cpp
+++ b/dom/push/PushManager.cpp
@@ -215,6 +215,7 @@ class GetSubscriptionRunnable final : public Runnable {
     AssertIsOnMainThread();
 
     nsCOMPtr<nsIPrincipal> principal;
+    nsCOMPtr<nsIPrincipal> effectiveStoragePrincipal;
 
     {
       // Bug 1228723: If permission is revoked or an error occurs, the
@@ -226,6 +227,7 @@ class GetSubscriptionRunnable final : public Runnable {
         return NS_OK;
       }
       principal = mProxy->GetWorkerPrivate()->GetPrincipal();
+      effectiveStoragePrincipal = mProxy->GetWorkerPrivate()->GetEffectiveStoragePrincipal();
     }
 
     MOZ_ASSERT(principal);
@@ -233,11 +235,13 @@ class GetSubscriptionRunnable final : public Runnable {
     RefPtr<GetSubscriptionCallback> callback =
         new GetSubscriptionCallback(mProxy, mScope);
 
-    PermissionState state;
-    nsresult rv = GetPermissionState(principal, state);
-    if (NS_FAILED(rv)) {
-      callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
-      return NS_OK;
+    PermissionState state = PermissionState::Denied;
+
+    if (effectiveStoragePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) {
+      if (NS_FAILED(GetPermissionState(principal, state))) {
+        callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
+        return NS_OK;
+      }
     }
 
     if (state != PermissionState::Granted) {
@@ -256,6 +260,7 @@ class GetSubscriptionRunnable final : public Runnable {
       return NS_OK;
     }
 
+    nsresult rv;
     if (mAction == PushManager::SubscribeAction) {
       if (mAppServerKey.IsEmpty()) {
         rv = service->Subscribe(mScope, principal, callback);
diff --git a/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini b/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini
deleted file mode 100644
index 9b31f82ff3a..00000000000
--- a/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[cross-origin-nested.tentative.https.sub.window.html]
-  [third party worker]
-    expected: FAIL
-
-  [nested first party worker]
-    expected: FAIL
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini b/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini
deleted file mode 100644
index 9b31f82ff3a..00000000000
--- a/testing/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[cross-origin-nested.tentative.https.sub.window.html]
-  [third party worker]
-    expected: FAIL
-
-  [nested first party worker]
-    expected: FAIL
Loading diff…