CVE-2026-16379
Overview
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
ExternalHelperAppParentrecording whether the load came from afile://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.
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
- Compromise a content process An attacker gains code execution in an ordinary (non-file) content process via a separate content-process bug.
- Forge the IPC message
The process sends
SendPExternalHelperAppConstructorwith afile://uripointing at a local file it should not control and a chosenaWasFileChannelvalue. - Bypass the origin check
The parent constructs
ExternalHelperAppParentand stores the spoofedmWasFileChannel, since no check bound thefile://URI toFILE_REMOTE_TYPE. - 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
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
| Function | Change | Notes |
|---|---|---|
mWasFileChanneluriloader/exthandler/ExternalHelperAppParent.cpp |
modified |
Files Changed
dom/ipc/ContentParent.cppdom/ipc/ContentParent.hdom/ipc/PContent.ipdluriloader/exthandler/ExternalHelperAppParent.cppuriloader/exthandler/ExternalHelperAppParent.huriloader/exthandler/nsExternalHelperAppService.cpp
Audit Directions
- Child-supplied trust flagsSearch 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 gatingAudit every parent-side handler that accepts an
nsIURIfrom a content process for aSchemeIs("file")/FILE_REMOTE_TYPEcheck consistent withProcessIsolationandValidatePrincipalCouldPotentiallyBeLoadedBy. - External-handler and download pathsReview
uriloader/exthandleractors 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.
Patch
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);