Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSite isolation issue in the Networking component
ComponentDOM
Bug ClassLogic Error
Tracker2043200
Fix commit0379190125a3 (firefox) +89/-31
CISA KEVNot listed
CreditedAtsushi Sada
Disclosed2026-07-21

Background

`RedirectChannelRegistrar`
A process-global singleton in the parent process that stores in-flight redirect channels keyed by a numeric id so a child process can later reclaim the parent side of the redirected channel.
`ContentParentId`
A unique identifier for a given content process (rendered as a raw uint64_t, with 0 denoting the parent process itself).
`connectParent`
The nsIChildChannel call, issued from a content process, that triggers NS_LinkRedirectChannels in the parent to pair a registered real channel with a parent-side actor.
Site isolation
The security boundary ensuring one content process cannot reach resources, channels, or data belonging to another origin’s process.

Root Cause Analysis

When a redirect channel was registered via RegisterChannel, the registrar stored only the mapping from the caller-supplied id to the nsIChannel, with no record of which content process the redirect was actually destined for. Later, LinkChannels looked up a channel purely by that id — an identifier that is supplied by (and thus attacker-controllable from) the requesting content process — and returned it to whatever process called connectParent, violating the invariant that a registered channel may only be handed to the process it was registered for. A malicious content process could therefore guess or reuse an id and obtain the parent-side channel belonging to a different process, breaking site isolation in the Networking component.

The fix threads a ContentParentId (aContentParentId) through registerChannel/linkChannels, records the owning process in a new mChannelOwners map at registration, and makes LinkChannels return NS_ERROR_NOT_AVAILABLE unless the requesting process id matches the recorded owner. This works because the owner is fixed by the trusted parent at registration time and the caller’s ChildID() is derived from the IPC actor rather than from attacker-supplied payload, so cross-process claims are rejected.

Key insight
The single core mistake was authorizing channel handoff on a content-process-supplied id alone with no ownership check, letting one process claim another’s redirect channel; the fix binds each registered channel to its owning ContentParentId and refuses to link it to any actor from a different process.

Attack Path

  1. Observe a redirect id A compromised or malicious content process learns or guesses the numeric id under which a redirect channel destined for another process is registered.
  2. Forge a connectParent request The malicious process calls connectParent(id), driving RecvExtProtocolChannelConnectParent (or the GeckoView path) with the target id it does not own.
  3. Reclaim the foreign channel Pre-fix, LinkChannels matches on id alone and returns the victim’s registered nsIChannel, linking it to the attacker’s parent actor.
  4. Cross the isolation boundary The attacker’s process now holds and receives notifications from a channel that belongs to a different process/origin, defeating site isolation for that networking channel.

Impact Assessment

An attacker who already controls a content process can obtain the parent side of a redirect channel registered for a different content process (or the parent process), giving them access to networking resources across the site-isolation boundary. The gain is confined to cross-process channel confusion within the browser’s Networking/DOM machinery and requires the attacker to first compromise or script a content process and to reference a valid registered redirect id. Rated moderate/medium severity with no memory-safety primitive implied by the diff.

Changed Functions

FunctionChangeNotes
RegisterChannel
netwerk/base/RedirectChannelRegistrar.cpp
modified
if
netwerk/ipc/DocumentLoadListener.cpp
modified
if
netwerk/ipc/ParentChannelWrapper.cpp
modified

Files Changed

  • docshell/base/nsDocShell.cpp
  • dom/ipc/ContentParent.cpp
  • mobile/android/components/geckoview/GeckoViewContentChannelParent.cpp
  • netwerk/base/RedirectChannelRegistrar.cpp
  • netwerk/base/RedirectChannelRegistrar.h
  • netwerk/base/nsIRedirectChannelRegistrar.idl
  • netwerk/base/nsNetUtil.cpp
  • netwerk/base/nsNetUtil.h
  • netwerk/ipc/DocumentLoadListener.cpp
  • netwerk/ipc/NeckoParent.cpp
  • netwerk/ipc/ParentChannelWrapper.cpp
  • netwerk/ipc/ParentChannelWrapper.h
  • netwerk/ipc/ParentProcessDocumentChannel.cpp
  • netwerk/protocol/http/HttpChannelParent.cpp

Audit Directions

  • Caller-supplied identifiers as capabilities
    Audit any registry keyed by an id that crosses an IPC boundary and confirm handoff is authorized by a trusted, actor-derived owner rather than by the untrusted id alone.
  • IDL signature changes for security parameters
    Review every implementer and caller of an interface like nsIRedirectChannelRegistrar when an owner/process parameter is added, ensuring no path passes a placeholder or stale id that weakens the check.
  • Ownership lifecycle parity
    Verify that any new ownership map is inserted, checked, and removed in lockstep with the primary object (register/link/deregister) so stale entries cannot be reclaimed or leak across processes.
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index f1b0b1f7b6c..9b78ed2be44 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -10433,7 +10433,9 @@ nsresult nsDocShell::OpenRedirectedChannel(nsDocShellLoadState* aLoadState) {
     // that forwards functionality as needed, and then we register
     // it under the provided identifier.
     RefPtr wrapper = MakeRefPtr<ParentChannelWrapper>(channel, loader);
-    wrapper->Register(aLoadState->GetPendingRedirectChannelRegistrarId());
+    // We're in the parent process, so the redirect is owned by the parent
+    // process (ContentParentId 0).
+    wrapper->Register(aLoadState->GetPendingRedirectChannelRegistrarId(), 0);
 
     mLoadGroup->AddRequest(channel, nullptr);
   } else if (nsCOMPtr<nsIChildChannel> childChannel =
diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp
index 517aa43bd05..a35f7e4e082 100644
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -4846,14 +4846,16 @@ mozilla::ipc::IPCResult ContentParent::RecvExtProtocolChannelConnectParent(
 
   // First get the real channel created before redirect on the parent.
   nsCOMPtr<nsIChannel> channel;
-  rv = NS_LinkRedirectChannels(registrarId, nullptr, getter_AddRefs(channel));
+  rv = NS_LinkRedirectChannels(registrarId, ChildID(), nullptr,
+                               getter_AddRefs(channel));
   NS_ENSURE_SUCCESS(rv, IPC_OK());
 
   nsCOMPtr<nsIParentChannel> parent = do_QueryInterface(channel, &rv);
   NS_ENSURE_SUCCESS(rv, IPC_OK());
 
   // The channel itself is its own (faked) parent, link it.
-  rv = NS_LinkRedirectChannels(registrarId, parent, getter_AddRefs(channel));
+  rv = NS_LinkRedirectChannels(registrarId, ChildID(), parent,
+                               getter_AddRefs(channel));
   NS_ENSURE_SUCCESS(rv, IPC_OK());
 
   // Signal the parent channel that it's a redirect-to parent.  This will
diff --git a/mobile/android/components/geckoview/GeckoViewContentChannelParent.cpp b/mobile/android/components/geckoview/GeckoViewContentChannelParent.cpp
index aa72b5131ca..01d3225f2bc 100644
--- a/mobile/android/components/geckoview/GeckoViewContentChannelParent.cpp
+++ b/mobile/android/components/geckoview/GeckoViewContentChannelParent.cpp
@@ -197,8 +197,10 @@ bool GeckoViewContentChannelParent::Init(
 bool GeckoViewContentChannelParent::Init(
     const GeckoViewContentChannelConnectArgs& aArgs) {
   nsCOMPtr<nsIChannel> channel;
-  nsresult rv =
-      NS_LinkRedirectChannels(aArgs.channelId(), this, getter_AddRefs(channel));
+  dom::ContentParentId cpId =
+      static_cast<dom::ContentParent*>(Manager()->Manager())->ChildID();
+  nsresult rv = NS_LinkRedirectChannels(aArgs.channelId(), cpId, this,
+                                        getter_AddRefs(channel));
   if (NS_SUCCEEDED(rv)) {
     mChannel = channel;
   }
diff --git a/netwerk/base/RedirectChannelRegistrar.cpp b/netwerk/base/RedirectChannelRegistrar.cpp
index 9aa687ed88d..8fbc36e4faa 100644
--- a/netwerk/base/RedirectChannelRegistrar.cpp
+++ b/netwerk/base/RedirectChannelRegistrar.cpp
@@ -34,10 +34,12 @@ RedirectChannelRegistrar::GetOrCreate() {
 }
 
 NS_IMETHODIMP
-RedirectChannelRegistrar::RegisterChannel(nsIChannel* channel, uint64_t id) {
+RedirectChannelRegistrar::RegisterChannel(nsIChannel* channel, uint64_t id,
+                                          uint64_t aContentParentId) {
   MutexAutoLock lock(mLock);
 
   mRealChannels.InsertOrUpdate(id, channel);
+  mChannelOwners.InsertOrUpdate(id, aContentParentId);
 
   return NS_OK;
 }
@@ -53,10 +55,19 @@ RedirectChannelRegistrar::GetRegisteredChannel(uint64_t id,
 }
 
 NS_IMETHODIMP
-RedirectChannelRegistrar::LinkChannels(uint64_t id, nsIParentChannel* channel,
+RedirectChannelRegistrar::LinkChannels(uint64_t id, uint64_t aContentParentId,
+                                       nsIParentChannel* channel,
                                        nsIChannel** _retval) {
   MutexAutoLock lock(mLock);
 
+  // Only hand back the registered channel if it was registered for the
+  // requesting content process, since the id is supplied by the content
+  // process.
+  uint64_t owner;
+  if (!mChannelOwners.Get(id, &owner) || owner != aContentParentId) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
   if (!mRealChannels.Get(id, _retval)) return NS_ERROR_NOT_AVAILABLE;
 
   mParentChannels.InsertOrUpdate(id, channel);
@@ -79,6 +90,7 @@ RedirectChannelRegistrar::DeregisterChannels(uint64_t id) {
 
   mRealChannels.Remove(id);
   mParentChannels.Remove(id);
+  mChannelOwners.Remove(id);
   return NS_OK;
 }
 
diff --git a/netwerk/base/RedirectChannelRegistrar.h b/netwerk/base/RedirectChannelRegistrar.h
index 1d5b14a4d71..48acea2c606 100644
--- a/netwerk/base/RedirectChannelRegistrar.h
+++ b/netwerk/base/RedirectChannelRegistrar.h
@@ -10,6 +10,7 @@
 #include "nsIChannel.h"
 #include "nsIParentChannel.h"
 #include "nsInterfaceHashtable.h"
+#include "nsTHashMap.h"
 #include "mozilla/Mutex.h"
 
 namespace mozilla {
@@ -35,6 +36,13 @@ class RedirectChannelRegistrar final : public nsIRedirectChannelRegistrar {
 
   ChannelHashtable mRealChannels MOZ_GUARDED_BY(mLock);
   ParentChannelHashtable mParentChannels MOZ_GUARDED_BY(mLock);
+
+  // Maps a registered channel id to the ContentParentId (as a raw uint64_t,
+  // 0 for the parent process) of the process the redirect is destined for.
+  // linkChannels refuses to pair a real channel with a parent actor coming
+  // from any other process.
+  nsTHashMap<nsUint64HashKey, uint64_t> mChannelOwners MOZ_GUARDED_BY(mLock);
+
   Mutex mLock;
 
   static StaticRefPtr<RedirectChannelRegistrar> gSingleton;
diff --git a/netwerk/base/nsIRedirectChannelRegistrar.idl b/netwerk/base/nsIRedirectChannelRegistrar.idl
index 26dfdf3c6f1..15e522bd883 100644
--- a/netwerk/base/nsIRedirectChannelRegistrar.idl
+++ b/netwerk/base/nsIRedirectChannelRegistrar.idl
@@ -15,30 +15,37 @@ interface nsIParentChannel;
  * See also nsIChildChannel and nsIParentChannel.
  */
 
-[scriptable, uuid (efa36ea2-5b07-46fc-9534-a5acb8b77b72)]
+[scriptable, uuid (1f2c5e0a-9b3d-4d8e-8c4a-6d7e0b1a2c3d)]
 interface nsIRedirectChannelRegistrar : nsISupports
 {
   /**
    * Register the redirect target channel. The passed id needs to be a
    * unique ID for that channel (see `nsContentUtils::GenerateLoadIdentifier`).
+   * aContentParentId identifies the content process the redirect is destined
+   * for (0 for the parent process); linkChannels will only pair the channel
+   * with a parent actor coming from that same process.
    *
    * Primarily used in ParentChannelListener::AsyncOnChannelRedirect to get
    * a channel id sent to the HttpChannelChild being redirected.
    */
-  void registerChannel(in nsIChannel channel, in uint64_t id);
+  void registerChannel(in nsIChannel channel, in uint64_t id,
+                       in uint64_t aContentParentId);
 
   /**
-   * First, search for the channel registered under the id.  If found return
-   * it.  Then, register under the same id the parent side of IPC protocol
-   * to let it be later grabbed back by the originator of the redirect and
-   * notifications from the real channel could be forwarded to this parent
-   * channel.
+   * First, search for the channel registered under the id.  If found, and the
+   * registered channel is owned by aContentParentId, return it. Then, register
+   * under the same id the parent side of IPC protocol to let it be later
+   * grabbed back by the originator of the redirect and notifications from the
+   * real channel could be forwarded to this parent channel. Fails with
+   * NS_ERROR_NOT_AVAILABLE if no channel is registered under the id for the
+   * given content process.
    *
    * Primarily used in parent side of an IPC protocol implementation
    * in reaction to nsIChildChannel.connectParent(id) called from the child
-   * process.
+   * process. aContentParentId is the process that made that request.
    */
-  nsIChannel linkChannels(in uint64_t id, in nsIParentChannel channel);
+  nsIChannel linkChannels(in uint64_t id, in uint64_t aContentParentId,
+                          in nsIParentChannel channel);
 
   /**
    * Returns back the channel previously registered under the ID with
diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp
index 9941a85331b..5b27e9190c2 100644
--- a/netwerk/base/nsNetUtil.cpp
+++ b/netwerk/base/nsNetUtil.cpp
@@ -2637,14 +2637,15 @@ bool NS_ShouldRemoveAuthHeaderOnRedirect(nsIChannel* aOldChannel,
   return NS_FAILED(rv);
 }
 
-nsresult NS_LinkRedirectChannels(uint64_t channelId,
+nsresult NS_LinkRedirectChannels(uint64_t channelId, uint64_t aContentParentId,
                                  nsIParentChannel* parentChannel,
                                  nsIChannel** _result) {
   nsCOMPtr<nsIRedirectChannelRegistrar> registrar =
       RedirectChannelRegistrar::GetOrCreate();
   MOZ_ASSERT(registrar);
 
-  return registrar->LinkChannels(channelId, parentChannel, _result);
+  return registrar->LinkChannels(channelId, aContentParentId, parentChannel,
+                                 _result);
 }
 
 nsILoadInfo::CrossOriginEmbedderPolicy
diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h
index c5131fca50b..8337dcdae11 100644
--- a/netwerk/base/nsNetUtil.h
+++ b/netwerk/base/nsNetUtil.h
@@ -871,7 +871,10 @@ bool NS_ShouldRemoveAuthHeaderOnRedirect(nsIChannel* aOldChannel,
                                          nsIChannel* aNewChannel,
                                          uint32_t aFlags);
 
-nsresult NS_LinkRedirectChannels(uint64_t channelId,
+// aContentParentId identifies the process requesting the link (0 for the
+// parent process). The link only succeeds if the channel was registered for
+// that same process.
+nsresult NS_LinkRedirectChannels(uint64_t channelId, uint64_t aContentParentId,
                                  nsIParentChannel* parentChannel,
                                  nsIChannel** _result);
 
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
index 394de6e372d..1df2115be38 100644
--- a/netwerk/ipc/DocumentLoadListener.cpp
+++ b/netwerk/ipc/DocumentLoadListener.cpp
@@ -2255,7 +2255,20 @@ DocumentLoadListener::RedirectToRealChannel(
     chan = vsc->GetInnerChannel();
   }
   mRedirectChannelId = nsContentUtils::GenerateLoadIdentifier();
-  MOZ_ALWAYS_SUCCEEDS(registrar->RegisterChannel(chan, mRedirectChannelId));
+
+  // Bind the registered channel to the content process the redirect is
+  // destined for (0 for the parent process), so only that process can link
+  // its parent channel to it.
+  uint64_t ownerContentParentId = 0;
+  if (aDestinationProcess) {
+    if (ContentParent* destCp = *aDestinationProcess) {
+      ownerContentParentId = destCp->ChildID();
+    }
+  } else if (mContentParent) {
+    ownerContentParentId = mContentParent->ChildID();
+  }
+  MOZ_ALWAYS_SUCCEEDS(registrar->RegisterChannel(chan, mRedirectChannelId,
+                                                 ownerContentParentId));
 
   if (aDestinationProcess) {
     RefPtr<ContentParent> cp = *aDestinationProcess;
diff --git a/netwerk/ipc/NeckoParent.cpp b/netwerk/ipc/NeckoParent.cpp
index baffbc5a7ad..ce3dfc68290 100644
--- a/netwerk/ipc/NeckoParent.cpp
+++ b/netwerk/ipc/NeckoParent.cpp
@@ -354,7 +354,9 @@ mozilla::ipc::IPCResult NeckoParent::RecvConnectBaseChannel(
       new nsBaseParentChannel(ContentParent::Cast(Manager())->GetRemoteType());
 
   nsCOMPtr<nsIChannel> channel;
-  NS_LinkRedirectChannels(channelId, parentChannel, getter_AddRefs(channel));
+  NS_LinkRedirectChannels(channelId,
+                          ContentParent::Cast(Manager())->ChildID(),
+                          parentChannel, getter_AddRefs(channel));
   return IPC_OK();
 }
 
diff --git a/netwerk/ipc/ParentChannelWrapper.cpp b/netwerk/ipc/ParentChannelWrapper.cpp
index 2b0b806add9..67ac37a5f53 100644
--- a/netwerk/ipc/ParentChannelWrapper.cpp
+++ b/netwerk/ipc/ParentChannelWrapper.cpp
@@ -17,7 +17,8 @@ namespace net {
 NS_IMPL_ISUPPORTS(ParentChannelWrapper, nsIParentChannel, nsIStreamListener,
                   nsIRequestObserver);
 
-void ParentChannelWrapper::Register(uint64_t aRegistrarId) {
+void ParentChannelWrapper::Register(uint64_t aRegistrarId,
+                                    uint64_t aContentParentId) {
   nsCOMPtr<nsIRedirectChannelRegistrar> registrar =
       RedirectChannelRegistrar::GetOrCreate();
   if (!registrar) {
@@ -25,8 +26,8 @@ void ParentChannelWrapper::Register(uint64_t aRegistrarId) {
     return;
   }
   nsCOMPtr<nsIChannel> dummy;
-  MOZ_ALWAYS_SUCCEEDS(
-      NS_LinkRedirectChannels(aRegistrarId, this, getter_AddRefs(dummy)));
+  MOZ_ALWAYS_SUCCEEDS(NS_LinkRedirectChannels(
+      aRegistrarId, aContentParentId, this, getter_AddRefs(dummy)));
 
 #ifdef DEBUG
   // The channel registered with the RedirectChannelRegistrar will be the inner
diff --git a/netwerk/ipc/ParentChannelWrapper.h b/netwerk/ipc/ParentChannelWrapper.h
index 52c4c14ca77..d940b81c0e0 100644
--- a/netwerk/ipc/ParentChannelWrapper.h
+++ b/netwerk/ipc/ParentChannelWrapper.h
@@ -17,8 +17,9 @@ class ParentChannelWrapper : public nsIParentChannel {
       : mChannel(aChannel), mListener(aListener) {}
 
   // Registers this nsIParentChannel wrapper with the RedirectChannelRegistrar
-  // and holds a reference.
-  void Register(uint64_t aRegistrarId);
+  // and holds a reference. aContentParentId identifies the process the redirect
+  // is destined for (0 for the parent process).
+  void Register(uint64_t aRegistrarId, uint64_t aContentParentId);
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIPARENTCHANNEL
diff --git a/netwerk/ipc/ParentProcessDocumentChannel.cpp b/netwerk/ipc/ParentProcessDocumentChannel.cpp
index c17d27e3252..6b5cc79bfcd 100644
--- a/netwerk/ipc/ParentProcessDocumentChannel.cpp
+++ b/netwerk/ipc/ParentProcessDocumentChannel.cpp
@@ -126,7 +126,9 @@ ParentProcessDocumentChannel::OnRedirectVerifyCallback(nsresult aResult) {
       RefPtr<ParentChannelWrapper> wrapper =
           new ParentChannelWrapper(channel, mListener);
 
-      wrapper->Register(mDocumentLoadListener->GetRedirectChannelId());
+      // This is a parent-process document load, so the redirect is owned by
+      // the parent process (ContentParentId 0).
+      wrapper->Register(mDocumentLoadListener->GetRedirectChannelId(), 0);
     }
   }
 
diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp
index f2e8d562495..2f000218324 100644
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -704,7 +704,8 @@ bool HttpChannelParent::ConnectChannel(const uint32_t& registrarId) {
        "[this=%p, id=%" PRIu32 "]\n",
        this, registrarId));
   nsCOMPtr<nsIChannel> channel;
-  rv = NS_LinkRedirectChannels(registrarId, this, getter_AddRefs(channel));
+  rv = NS_LinkRedirectChannels(registrarId, GetContentParentId(), this,
+                               getter_AddRefs(channel));
   if (NS_FAILED(rv)) {
     NS_WARNING("Could not find the http channel to connect its IPC parent");
     // This makes the channel delete itself safely.  It's the only thing
@@ -1829,7 +1830,8 @@ HttpChannelParent::StartRedirect(nsIChannel* newChannel, uint32_t redirectFlags,
   }
 
   mRedirectChannelId = nsContentUtils::GenerateLoadIdentifier();
-  rv = registrar->RegisterChannel(newChannel, mRedirectChannelId);
+  rv = registrar->RegisterChannel(newChannel, mRedirectChannelId,
+                                  GetContentParentId());
   NS_ENSURE_SUCCESS(rv, rv);
 
   LOG(("Registered %p channel under id=%" PRIx64, newChannel,
@@ -1891,8 +1893,8 @@ HttpChannelParent::StartRedirect(nsIChannel* newChannel, uint32_t redirectFlags,
 
       // Re-link the HttpChannelParent to the new channel.
       nsCOMPtr<nsIChannel> linkedChannel;
-      rv = NS_LinkRedirectChannels(mRedirectChannelId, this,
-                                   getter_AddRefs(linkedChannel));
+      rv = NS_LinkRedirectChannels(mRedirectChannelId, GetContentParentId(),
+                                   this, getter_AddRefs(linkedChannel));
       NS_ENSURE_SUCCESS(rv, rv);
       MOZ_ASSERT(linkedChannel == newChannel);
 
Loading diff…