CVE-2026-74972
Overview
Background
- Push Subscriptions
- A DOM API (
PushManager) that lets a service worker register with a push service and later retrieve its existing subscription viagetSubscription(). - Effective storage principal
- The principal Gecko uses for storage-scoped decisions, carrying
OriginAttributesincluding 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.
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
- 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.
- Run a service worker The embedded partitioned context registers or reuses a service worker for the victim origin.
- Call getSubscription
The worker invokes
getSubscription(), drivingGetSubscriptionRunnable::Runon the main thread. - Bypass partitioning
The permission check runs against the unpartitioned
principal, returningGranted, so the first-party subscription is fetched and handed back to the partitioned context. - Exfiltrate subscription The attacker-controlled embed reads the disclosed subscription details, leaking cross-partition state.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/push/PushManager.cpp |
modified |
Files Changed
dom/push/PushManager.cpptesting/web-platform/meta/push-api/cross-origin-nested.tentative.https.sub.window.js.ini
Audit Directions
- Principal vs. storage principalAudit any state or permission check that uses a bare
principalwhere a partition-awareGetEffectiveStoragePrincipal()should govern storage-scoped data access. - Partition key gatingLook for storage/permission lookups that fail to inspect
OriginAttributesRef().mPartitionKey, which can let third-party partitioned contexts read first-party state. - Default-deny initializationVerify 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.
Patch
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
Regression Test / PoC
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