Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the DOM: Navigation component
ComponentDOM
Bug ClassLogic Error
Tracker2008369
Fix commit66e9354ea8bb (firefox) +18/-11
CISA KEVNot listed
CreditedSteven Julian
Disclosed2026-07-21

Background

`RecvLoadURIExternal`
the parent-process IPC handler in ContentParent that services a content process’s request to open a URI with an external protocol handler.
`nsIPrincipal`
Gecko’s security-context object that identifies the origin/authority whose privileges an action is performed under.
`ValidatePrincipal`
a ContentParent helper that checks an IPC-supplied principal is legitimate for the sending content process before it is trusted.
`NotNull<T>`
a Mozilla wrapper type that statically and dynamically enforces a pointer argument is non-null across the IPDL boundary.

Root Cause Analysis

The LoadURIExternal IPDL message declared both uri and triggeringPrincipal as nullable, and RecvLoadURIExternal forwarded the content-process-supplied aTriggeringPrincipal (and aRedirectPrincipal) straight into nsExternalHelperAppService::LoadURI without calling ValidatePrincipal on it. A compromised or malicious content process could therefore hand the trusted parent an arbitrary, spoofed, or null triggering principal, violating the invariant that any principal used to authorize a privileged action in the parent must actually belong to the sending process. Because that principal governs the external-protocol load’s authority and the same-tree spoofing checks inside LoadURI, an unvalidated value amounted to a privilege escalation in the parent process.

The fix adds ValidatePrincipal(aTriggeringPrincipal) and ValidatePrincipal(aRedirectPrincipal, {AllowNullPtr}) checks that fail the IPC channel via PrincipalValidationIpcFail when validation fails. It also tightens the contract by making uri and triggeringPrincipal NotNull/non-nullable and adding NS_ENSURE_ARG_POINTER(aTriggeringPrincipal) in LoadURI, so a triggering principal can no longer be null or attacker-forged.

Key insight
The core mistake was trusting a content-process-supplied triggering principal to authorize an external-protocol navigation in the parent without ever validating that the principal belonged to that process; the fix inserts the standard ValidatePrincipal gate and makes the principal non-nullable so forged or null values are rejected at the IPC boundary.

Attack Path

  1. Compromise a content process The attacker gains code execution in a sandboxed renderer via a prior content-process bug.
  2. Forge an IPC message The content process sends LoadURIExternal with a triggeringPrincipal (and optional redirectPrincipal) it does not legitimately own, or a null one.
  3. Reach the unguarded handler RecvLoadURIExternal accepts the message without calling ValidatePrincipal, so the spoofed principal is treated as authoritative in the parent.
  4. Bypass origin restrictions The forged principal is passed to nsExternalHelperAppService::LoadURI, where it drives the external-protocol dispatch and short-circuits the same-tree anti-spoofing checks.
  5. Escalate privilege The load proceeds under an authority the content process should not possess, achieving privilege escalation in the DOM navigation path.

Impact Assessment

An attacker who already controls a content process gains the ability to launch external-protocol URI loads in the parent process under a spoofed or null triggering principal, escalating beyond the content sandbox’s authority. This occurs in the trusted parent process via the PContent IPC channel and affects the DOM Navigation / external-handler path. The precondition is a pre-compromised content process able to send crafted IPC; the rated impact is moderate.

Changed Functions

FunctionChangeNotes
if
dom/ipc/ContentParent.cpp
modified

Files Changed

  • dom/ipc/ContentParent.cpp
  • dom/ipc/ContentParent.h
  • dom/ipc/PContent.ipdl
  • uriloader/exthandler/nsExternalHelperAppService.cpp

Audit Directions

  • Unvalidated IPC principals
    Audit every ContentParent::Recv* handler that consumes an nsIPrincipal from a content process and confirm it calls ValidatePrincipal before the principal authorizes any action.
  • Nullable security-sensitive IPDL args
    Review nullable nsIPrincipal/nullable nsIURI declarations in PContent.ipdl and related protocols; where the value must be trusted, prefer non-nullable/NotNull and explicit AllowNullPtr only where truly optional.
  • Principal forwarding into services
    Trace principals passed from IPC into privileged services like nsExternalHelperAppService::LoadURI to ensure origin/same-tree checks are not bypassable by a caller-controlled or null principal.
diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp
index c6b9c9e3fe9..517aa43bd05 100644
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -4809,7 +4809,7 @@ mozilla::ipc::IPCResult ContentParent::RecvAccumulateMixedContentHSTS(
 }
 
 mozilla::ipc::IPCResult ContentParent::RecvLoadURIExternal(
-    nsIURI* uri, nsIPrincipal* aTriggeringPrincipal,
+    NotNull<nsIURI*> uri, NotNull<nsIPrincipal*> aTriggeringPrincipal,
     nsIPrincipal* aRedirectPrincipal,
     const MaybeDiscarded<BrowsingContext>& aContext,
     bool aWasExternallyTriggered, bool aHasValidUserGestureActivation,
@@ -4818,16 +4818,21 @@ mozilla::ipc::IPCResult ContentParent::RecvLoadURIExternal(
     return IPC_OK();
   }
 
+  if (!ValidatePrincipal(aTriggeringPrincipal)) {
+    return PrincipalValidationIpcFail(aTriggeringPrincipal, this, __func__);
+  }
+
+  if (!ValidatePrincipal(aRedirectPrincipal,
+                         {ValidatePrincipalOptions::AllowNullPtr})) {
+    return PrincipalValidationIpcFail(aRedirectPrincipal, this, __func__);
+  }
+
   nsCOMPtr<nsIExternalProtocolService> extProtService(
       do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID));
   if (!extProtService) {
     return IPC_OK();
   }
 
-  if (!uri) {
-    return IPC_FAIL(this, "uri must not be null.");
-  }
-
   BrowsingContext* bc = aContext.get();
   extProtService->LoadURI(uri, aTriggeringPrincipal, aRedirectPrincipal, bc,
                           aWasExternallyTriggered,
diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h
index 060864c020a..6a0559b9e3c 100644
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -1038,7 +1038,7 @@ class ContentParent final : public PContentParent,
   mozilla::ipc::IPCResult RecvSetURITitle(nsIURI* uri, const nsAString& title);
 
   mozilla::ipc::IPCResult RecvLoadURIExternal(
-      nsIURI* uri, nsIPrincipal* triggeringPrincipal,
+      NotNull<nsIURI*> uri, NotNull<nsIPrincipal*> triggeringPrincipal,
       nsIPrincipal* redirectPrincipal,
       const MaybeDiscarded<BrowsingContext>& aContext,
       bool aWasExternallyTriggered, bool aHasValidUserGestureActivation,
diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl
index d328a07a8e6..ebcde0b2b2c 100644
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -1202,8 +1202,8 @@ parent:
     async StartVisitedQueries(nullable nsIURI[] uri);
     async SetURITitle(nullable nsIURI uri, nsString title);
 
-    async LoadURIExternal(nullable nsIURI uri,
-                          nullable nsIPrincipal triggeringPrincipal,
+    async LoadURIExternal(nsIURI uri,
+                          nsIPrincipal triggeringPrincipal,
                           nullable nsIPrincipal redirectPrincipal,
                           MaybeDiscardedBrowsingContext browsingContext,
                           bool wasExternallyTriggered,
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
index d36dc1617cd..862b6cba4ee 100644
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -1036,11 +1036,13 @@ nsExternalHelperAppService::LoadURI(nsIURI* aURI,
                                     bool aHasValidUserGestureActivation,
                                     bool aNewWindowTarget) {
   NS_ENSURE_ARG_POINTER(aURI);
+  NS_ENSURE_ARG_POINTER(aTriggeringPrincipal);
 
   if (XRE_IsContentProcess()) {
     mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
-        aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
-        aTriggeredExternally, aHasValidUserGestureActivation, aNewWindowTarget);
+        WrapNotNull(aURI), WrapNotNull(aTriggeringPrincipal),
+        aRedirectPrincipal, aBrowsingContext, aTriggeredExternally,
+        aHasValidUserGestureActivation, aNewWindowTarget);
     return NS_OK;
   }
 
@@ -1108,7 +1110,7 @@ nsExternalHelperAppService::LoadURI(nsIURI* aURI,
   // links can always navigate everywhere, so this is a minor additional
   // restriction, only aiming to prevent some types of spoofing attacks
   // from otherwise disjoint browsingcontext trees.
-  if (aBrowsingContext && aTriggeringPrincipal &&
+  if (aBrowsingContext &&
       // Add-on principals are always allowed:
       !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
       // As is chrome code:
Loading diff…