CVE-2026-74963
Overview
Background
- CookieStore API
- a modern asynchronous DOM interface (
dom/cookiestore) that lets pages and service workers read cookies and subscribe to cookie-change notifications for a given scope URL. - `CookieStoreParent`
- the privileged parent-process IPC endpoint that services
RecvGetSubscriptionsRequestandRecvSubscribeOrUnsubscribeRequestmessages sent from content processes. - `nsIPrincipal::IsSameOrigin`
- a method that compares a principal’s origin against a given
nsIURI, returning true only when scheme, host, and port match under the same-origin policy. - `CookieCommons::DomainMatches`
- a helper that tests whether a given
Cookieapplies to a specific host, used to gate delivery of cookie-change notifications.
Root Cause Analysis
The CookieStoreParent::RecvGetSubscriptionsRequest and CookieStoreParent::RecvSubscribeOrUnsubscribeRequest handlers accepted an attacker-controllable aScopeURL from the content process and used it to register and query cookie subscriptions without ever verifying that the URL’s origin matched the caller’s principal. Although the code validated that the principal was allowed for the process’s remote type, it never enforced that the subscription scope was same-origin with that principal, violating the same-origin invariant that a page may only observe cookies for its own origin. As a consequence, a content process could subscribe to and enumerate cookie-change subscriptions scoped to a foreign origin.
The fix introduces SubscriptionPrincipalMatchesScope, which parses aScopeURL into an nsIURI and calls principal->IsSameOrigin(scopeURI), causing both IPC handlers to IPC_FAIL when the principal is not same-origin with the requested scope. Complementary hardening in CookieStoreSubscriptionService::Observe re-derives the subscription’s host with nsContentUtils::GetHostOrIPv6WithBrackets and gates notification delivery on CookieCommons::DomainMatches, and CookieStoreNotifier::Create switches from GetAsciiHost to GetHostOrIPv6WithBrackets so IPv6 literal hosts are normalized consistently. Together these ensure both the registration and the delivery paths honor origin/host boundaries.
aScopeURL without confirming it was same-origin with the authenticated principal, so a page could register or query cookie subscriptions for arbitrary origins. The fix closes this by adding an explicit IsSameOrigin check (SubscriptionPrincipalMatchesScope) at the IPC boundary and a DomainMatches host gate at notification-delivery time.Attack Path
- Establish a content process An attacker gets script running in a normal page or service worker that can issue CookieStore IPC requests to the parent.
- Craft a foreign scope URL
The script sends
RecvSubscribeOrUnsubscribeRequestorRecvGetSubscriptionsRequestwith anaScopeURLpointing at a victim origin rather than its own. - Bypass the missing origin check
The parent validates only the principal-to-remote-type mapping, not that
aScopeURLmatches the principal, so the request is accepted. - Observe cross-origin cookie activity The attacker enumerates or subscribes to cookie-change notifications tied to the victim origin, leaking cookie state that should be same-origin restricted.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
CookieStoreParentdom/cookiestore/CookieStoreParent.cpp |
modified | |
fordom/cookiestore/CookieStoreSubscriptionService.cpp |
modified |
Files Changed
dom/cookiestore/CookieStoreNotifier.cppdom/cookiestore/CookieStoreParent.cppdom/cookiestore/CookieStoreSubscriptionService.cpp
Audit Directions
- Missing origin binding on IPC scope parametersAudit other parent-process IPC handlers that accept a URL, scope, or domain from the child and confirm each verifies same-origin (or equivalent) against the authenticated principal rather than trusting the supplied value.
- Registration versus delivery gapsWhere a subscribe/observe pattern exists, check that both the registration path and the notification-delivery path enforce the origin/host boundary, since either alone can be insufficient.
- Inconsistent host normalizationLook for mixed use of
GetAsciiHostversusGetHostOrIPv6WithBrackets(or other host extraction) in matching comparisons, since IPv6 bracket handling mismatches can defeatDomainMatches-style checks.
Patch
diff --git a/dom/cookiestore/CookieStoreNotifier.cpp b/dom/cookiestore/CookieStoreNotifier.cpp
index 2aeac8c2d49..e127ba4b64b 100644
--- a/dom/cookiestore/CookieStoreNotifier.cpp
+++ b/dom/cookiestore/CookieStoreNotifier.cpp
@@ -10,6 +10,7 @@
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/net/Cookie.h"
#include "mozilla/net/CookieCommons.h"
+#include "nsContentUtils.h"
#include "nsGlobalWindowInner.h"
#include "nsICookie.h"
#include "nsICookieNotification.h"
@@ -49,7 +50,9 @@ already_AddRefed<CookieStoreNotifier> CookieStoreNotifier::Create(
}
nsCString host;
- if (NS_WARN_IF(NS_FAILED(principal->GetAsciiHost(host))) || host.IsEmpty()) {
+ if (NS_WARN_IF(NS_FAILED(
+ nsContentUtils::GetHostOrIPv6WithBrackets(principal, host))) ||
+ host.IsEmpty()) {
return nullptr;
}
diff --git a/dom/cookiestore/CookieStoreParent.cpp b/dom/cookiestore/CookieStoreParent.cpp
index b9a51c2d18f..8620a653f63 100644
--- a/dom/cookiestore/CookieStoreParent.cpp
+++ b/dom/cookiestore/CookieStoreParent.cpp
@@ -24,6 +24,7 @@
#include "nsICookieManager.h"
#include "nsICookieService.h"
#include "nsIEffectiveTLDService.h"
+#include "nsNetUtil.h"
#include "nsProxyRelease.h"
using namespace mozilla::ipc;
@@ -65,6 +66,16 @@ bool CheckContentProcessSecurity(ThreadsafeContentParentHandle* aParent,
return cs->ContentProcessHasCookie(aDomain, aOriginAttributes);
}
+bool SubscriptionPrincipalMatchesScope(nsIPrincipal* aPrincipal,
+ const nsACString& aScopeURL) {
+ nsCOMPtr<nsIURI> scopeURI;
+ if (NS_WARN_IF(NS_FAILED(NS_NewURI(getter_AddRefs(scopeURI), aScopeURL)))) {
+ return false;
+ }
+
+ return aPrincipal->IsSameOrigin(scopeURI);
+}
+
} // namespace
CookieStoreParent::CookieStoreParent() { AssertIsOnBackgroundThread(); }
@@ -218,6 +229,10 @@ mozilla::ipc::IPCResult CookieStoreParent::RecvGetSubscriptionsRequest(
return IPC_FAIL(this, "principal not allowed for remote type");
}
+ if (!SubscriptionPrincipalMatchesScope(principal, aScopeURL)) {
+ return IPC_FAIL(this, "principal not same-origin with scope");
+ }
+
InvokeAsync(GetMainThreadSerialEventTarget(), __func__,
[self = RefPtr(this), aPrincipalInfo, aScopeURL]() {
CookieStoreSubscriptionService* service =
@@ -268,6 +283,10 @@ mozilla::ipc::IPCResult CookieStoreParent::RecvSubscribeOrUnsubscribeRequest(
return IPC_FAIL(this, "principal not allowed for remote type");
}
+ if (!SubscriptionPrincipalMatchesScope(principal, aScopeURL)) {
+ return IPC_FAIL(this, "principal not same-origin with scope");
+ }
+
InvokeAsync(GetMainThreadSerialEventTarget(), __func__,
[self = RefPtr(this), aPrincipalInfo, aScopeURL, aSubscriptions,
aSubscription]() {
diff --git a/dom/cookiestore/CookieStoreSubscriptionService.cpp b/dom/cookiestore/CookieStoreSubscriptionService.cpp
index 004375623f5..2ee66f224ee 100644
--- a/dom/cookiestore/CookieStoreSubscriptionService.cpp
+++ b/dom/cookiestore/CookieStoreSubscriptionService.cpp
@@ -12,6 +12,7 @@
#include "mozilla/net/Cookie.h"
#include "mozilla/net/CookieCommons.h"
#include "nsAppDirectoryServiceDefs.h"
+#include "nsContentUtils.h"
#include "nsICookieNotification.h"
using namespace mozilla::dom;
@@ -321,6 +322,22 @@ CookieStoreSubscriptionService::Observe(nsISupports* aSubject,
continue;
}
+ nsCOMPtr<nsIURI> principalURI;
+ rv = NS_NewURI(getter_AddRefs(principalURI), principalInfo.spec());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsAutoCString host;
+ rv = nsContentUtils::GetHostOrIPv6WithBrackets(principalURI, host);
+ if (NS_WARN_IF(NS_FAILED(rv)) || host.IsEmpty()) {
+ continue;
+ }
+
+ if (!CookieCommons::DomainMatches(Cookie::Cast(cookie), host)) {
+ continue;
+ }
+
for (const CookieSubscription& subscription : data.mSubscriptions) {
if (subscription.name().isSome() && subscription.name().value() != name) {
continue;