Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the DOM: Content Processes component
ComponentDOM
Bug ClassLogic Error
Tracker2039452
Fix commit7f4591110004 (firefox) +28/-20
CISA KEVNot listed
CreditedShu Takahashi
Disclosed2026-07-21

Background

`PExternalHelperApp`
the IPDL protocol a content process uses to ask the parent process to hand a download off to an external helper application.
`ExternalHelperAppParent`
the parent-side actor that receives a would-be download and drives the native helper-app launch, exposing WasFileChannel().
`FILE_REMOTE_TYPE`
the remote-process type reserved for the dedicated file content process that is the only process permitted to load file:// URIs.
`mWasFileChannel`
a boolean on ExternalHelperAppParent recording whether the load came from a file:// channel, consulted when deciding native local-file handling.

Root Cause Analysis

The RecvPExternalHelperAppConstructor path in ContentParent.cpp accepted both a uri and a child-supplied aWasFileChannel boolean, and ExternalHelperAppParent stored that boolean verbatim into mWasFileChannel. Because the flag and the URI both originated in the content process, a compromised non-file process could send a file:// uri together with an attacker-chosen aWasFileChannel value, violating the invariant that a file:// load may only be driven by the dedicated file content process (FILE_REMOTE_TYPE) as enforced elsewhere by ValidatePrincipalCouldPotentiallyBeLoadedBy and ProcessIsolation. The parent trusted the child’s assertion about the channel type instead of deriving it from data it could authenticate.

The fix rejects any file:// uri arriving from a process other than the file process (returning IPC_FAIL), and stops trusting the child by recomputing mWasFileChannel directly from the URI scheme. Together these ensure the native helper-launch decision is bound to a URI the process was actually allowed to open rather than to a spoofable flag.

Key insight
The single mistake was trusting a content-process-supplied aWasFileChannel flag (and an unvalidated file:// uri) to gate a privileged local-file helper launch; the fix removes the flag from IPC, derives the value from the parent-visible URI scheme, and refuses file:// URIs from any process that is not the file content process.

Attack Path

  1. Compromise a content process An attacker gains code execution in an ordinary (non-file) content process via a separate content-process bug.
  2. Forge the IPC message The process sends SendPExternalHelperAppConstructor with a file:// uri pointing at a local file it should not control and a chosen aWasFileChannel value.
  3. Bypass the origin check The parent constructs ExternalHelperAppParent and stores the spoofed mWasFileChannel, since no check bound the file:// URI to FILE_REMOTE_TYPE.
  4. Drive local-file handling The parent treats the load as a legitimate file-channel download and proceeds toward native helper-app handling of the attacker-selected local file, escalating the sandboxed process’s effective privilege.

Impact Assessment

An attacker who already controls a sandboxed content process can escape the intended file-loading restriction and cause the more-privileged parent process to treat an attacker-chosen file:// URI as a trusted local-file download, driving native helper-app handling it should not be allowed to trigger. This is a privilege escalation within the browser’s process model rather than remote code execution on its own, and it requires the precondition of an already-compromised non-file content process. Impact is rated moderate/medium per the metadata.

Changed Functions

FunctionChangeNotes
mWasFileChannel
uriloader/exthandler/ExternalHelperAppParent.cpp
modified

Files Changed

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

Audit Directions

  • Child-supplied trust flags
    Search other IPDL messages for boolean or type flags sent from content to parent that gate privileged behavior, and verify the parent re-derives them from data it can authenticate.
  • `file://` and scheme gating
    Audit every parent-side handler that accepts an nsIURI from a content process for a SchemeIs("file") / FILE_REMOTE_TYPE check consistent with ProcessIsolation and ValidatePrincipalCouldPotentiallyBeLoadedBy.
  • External-handler and download paths
    Review uriloader/exthandler actors that trigger native helper-app or local-file handling to confirm the launch decision is bound to a URI the sending process was actually permitted to load.
diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp
index 3538da8e512..c9af3b237e2 100644
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -61,6 +61,7 @@
 #include "mozilla/ScriptPreloader.h"
 #include "mozilla/Services.h"
 #include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_browser.h"
 #include "mozilla/StaticPrefs_dom.h"
 #include "mozilla/StaticPrefs_fission.h"
 #include "mozilla/StaticPrefs_media.h"
@@ -4710,11 +4711,11 @@ ContentParent::AllocPExternalHelperAppParent(
     const nsACString& aMimeContentType, const nsACString& aContentDisposition,
     const uint32_t& aContentDispositionHint,
     const nsAString& aContentDispositionFilename, const bool& aForceSave,
-    const int64_t& aContentLength, const bool& aWasFileChannel,
-    nsIURI* aReferrer, const MaybeDiscarded<BrowsingContext>& aContext) {
+    const int64_t& aContentLength, nsIURI* aReferrer,
+    const MaybeDiscarded<BrowsingContext>& aContext) {
   RefPtr<ExternalHelperAppParent> parent = new ExternalHelperAppParent(
-      uri, aContentLength, aWasFileChannel, aContentDisposition,
-      aContentDispositionHint, aContentDispositionFilename);
+      uri, aContentLength, aContentDisposition, aContentDispositionHint,
+      aContentDispositionFilename);
   return parent.forget();
 }
 
@@ -4724,8 +4725,20 @@ mozilla::ipc::IPCResult ContentParent::RecvPExternalHelperAppConstructor(
     const nsACString& aContentDisposition,
     const uint32_t& aContentDispositionHint,
     const nsAString& aContentDispositionFilename, const bool& aForceSave,
-    const int64_t& aContentLength, const bool& aWasFileChannel,
-    nsIURI* aReferrer, const MaybeDiscarded<BrowsingContext>& aContext) {
+    const int64_t& aContentLength, nsIURI* aReferrer,
+    const MaybeDiscarded<BrowsingContext>& aContext) {
+  // A content process must never be able to drive a native helper-app launch
+  // of a local file it chose: that decision has to be bound to a channel the
+  // process was actually allowed to open. A file:// URI can only be loaded in
+  // a file content process (mirrors ValidatePrincipalCouldPotentiallyBeLoadedBy
+  // and the sandboxing policy enforced in ProcessIsolation), so reject a
+  // file:// URI coming from any other process.
+  if (uri && uri->SchemeIs("file") &&
+      StaticPrefs::browser_tabs_remote_separateFileUriProcess() &&
+      GetRemoteType() != FILE_REMOTE_TYPE) {
+    return IPC_FAIL(this, "Non-file process sent a file:// URI.");
+  }
+
   BrowsingContext* context = aContext.IsDiscarded() ? nullptr : aContext.get();
   if (!static_cast<ExternalHelperAppParent*>(actor)->Init(
           loadInfoArgs, aMimeContentType, aForceSave, aReferrer, context)) {
diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h
index 6a0559b9e3c..ecbd84b707d 100644
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -947,8 +947,8 @@ class ContentParent final : public PContentParent,
       const nsACString& aMimeContentType, const nsACString& aContentDisposition,
       const uint32_t& aContentDispositionHint,
       const nsAString& aContentDispositionFilename, const bool& aForceSave,
-      const int64_t& aContentLength, const bool& aWasFileChannel,
-      nsIURI* aReferrer, const MaybeDiscarded<BrowsingContext>& aContext);
+      const int64_t& aContentLength, nsIURI* aReferrer,
+      const MaybeDiscarded<BrowsingContext>& aContext);
 
   mozilla::ipc::IPCResult RecvPExternalHelperAppConstructor(
       PExternalHelperAppParent* actor, nsIURI* uri,
@@ -956,8 +956,7 @@ class ContentParent final : public PContentParent,
       const nsACString& aContentDisposition,
       const uint32_t& aContentDispositionHint,
       const nsAString& aContentDispositionFilename, const bool& aForceSave,
-      const int64_t& aContentLength, const bool& aWasFileChannel,
-      nsIURI* aReferrer,
+      const int64_t& aContentLength, nsIURI* aReferrer,
       const MaybeDiscarded<BrowsingContext>& aContext) override;
 
   already_AddRefed<PHandlerServiceParent> AllocPHandlerServiceParent();
diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl
index ebcde0b2b2c..c298d3e834e 100644
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -1240,7 +1240,6 @@ parent:
                              nsString aContentDispositionFilename,
                              bool aForceSave,
                              int64_t aContentLength,
-                             bool aWasFileChannel,
                              nullable nsIURI aReferrer,
                              MaybeDiscardedBrowsingContext aContext);
 
diff --git a/uriloader/exthandler/ExternalHelperAppParent.cpp b/uriloader/exthandler/ExternalHelperAppParent.cpp
index 912b4f4a27b..376f24c68fb 100644
--- a/uriloader/exthandler/ExternalHelperAppParent.cpp
+++ b/uriloader/exthandler/ExternalHelperAppParent.cpp
@@ -31,7 +31,7 @@ NS_IMPL_ISUPPORTS_INHERITED(ExternalHelperAppParent, nsHashPropertyBag,
                             nsIStreamListener, nsIExternalHelperAppParent)
 
 ExternalHelperAppParent::ExternalHelperAppParent(
-    nsIURI* uri, const int64_t& aContentLength, const bool& aWasFileChannel,
+    nsIURI* uri, const int64_t& aContentLength,
     const nsACString& aContentDispositionHeader,
     const uint32_t& aContentDispositionHint,
     const nsAString& aContentDispositionFilename)
@@ -42,7 +42,9 @@ ExternalHelperAppParent::ExternalHelperAppParent(
       mStatus(NS_OK),
       mCanceled(false),
       mContentLength(aContentLength),
-      mWasFileChannel(aWasFileChannel) {
+      // Never trust a child-supplied flag for the native helper-launch
+      // decision: derive it from the actual URI the parent will operate on.
+      mWasFileChannel(uri && uri->SchemeIs("file")) {
   mContentDispositionHeader = aContentDispositionHeader;
   if (!mContentDispositionHeader.IsEmpty()) {
     NS_GetFilenameFromDisposition(mContentDispositionFilename,
diff --git a/uriloader/exthandler/ExternalHelperAppParent.h b/uriloader/exthandler/ExternalHelperAppParent.h
index d47a3f7bf27..b726be721f5 100644
--- a/uriloader/exthandler/ExternalHelperAppParent.h
+++ b/uriloader/exthandler/ExternalHelperAppParent.h
@@ -71,7 +71,6 @@ class ExternalHelperAppParent
   bool WasFileChannel() override { return mWasFileChannel; }
 
   ExternalHelperAppParent(nsIURI* uri, const int64_t& contentLength,
-                          const bool& wasFileChannel,
                           const nsACString& aContentDispositionHeader,
                           const uint32_t& aContentDispositionHint,
                           const nsAString& aContentDispositionFilename);
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
index 862b6cba4ee..b39823443c8 100644
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -720,7 +720,6 @@ nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
   nsCString disp;
   nsCOMPtr<nsIURI> uri;
   int64_t contentLength = -1;
-  bool wasFileChannel = false;
   uint32_t contentDisposition = -1;
   nsAutoString fileName;
   nsCOMPtr<nsILoadInfo> loadInfo;
@@ -732,9 +731,6 @@ nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
   aChannel->GetContentDispositionHeader(disp);
   loadInfo = aChannel->LoadInfo();
 
-  nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aChannel));
-  wasFileChannel = fileChan != nullptr;
-
   nsCOMPtr<nsIURI> referrer;
   NS_GetReferrerFromChannel(aChannel, getter_AddRefs(referrer));
 
@@ -748,8 +744,8 @@ nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
   RefPtr childListener = MakeRefPtr<ExternalHelperAppChild>();
   MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
       childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
-      contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
-      referrer, aContentContext));
+      contentDisposition, fileName, aForceSave, contentLength, referrer,
+      aContentContext));
 
   NS_ADDREF(*aStreamListener = childListener);
 
Loading diff…