Low CVSS 5.3 firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionBy checking the result of calls to `window.open` with specifically set protocol handlers, an attacker could determine if the application which implements that protocol handler is installed.
ComponentNetworking
Bug ClassLogic Error
Tracker1881037
Fix commit4a09bbc478eb (firefox) +17/-31
CWECWE-203
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedSatoki Tsuji
Disclosed2024-10-01

Background

`nsDocShell::FilterStatusForErrorPage`
The nsDocShell helper that decides whether a failed network load should be turned into a visible error page or silently swallowed.
`NS_ERROR_UNKNOWN_PROTOCOL`
The status code emitted when a navigation targets a URI whose scheme has no registered handler (for example a custom external-protocol handler).
Initial document
The empty placeholder document a BrowsingContext holds before any real content loads, reported by IsInitialDocument; a freshly created window.open popup starts in this state.
System principal
The most-privileged security principal, identified by IsSystemPrincipal, used for navigations the browser itself triggers rather than page-triggered ones.

Root Cause Analysis

The vulnerable path is in nsDocShell::FilterStatusForErrorPage, which for NS_ERROR_UNKNOWN_PROTOCOL normally suppresses the error page (and reports the skip via aSkippedUnknownProtocolNavigation) whenever the load is not IsSystemPrincipal, so that ordinary page-triggered navigations to unknown protocols do nothing observable. The code, however, carried an extra aIsInitialDocument exception: if the target BrowsingContext was still showing its initial document, the error page was shown even for a page-triggered load. Because a popup opened with window.open begins on its initial document, script could point that popup at a protocol-handler URI and observe divergent behavior — an error page versus a silent success — thereby learning whether a handler (and thus the associated application) was installed, violating the invariant that page-triggered unknown-protocol loads must be indistinguishable regardless of handler presence.

The fix removes the aIsInitialDocument parameter entirely from FilterStatusForErrorPage (in nsDocShell.cpp and nsDocShell.h) and from both call sites, so the suppression now depends only on whether the triggering principal is the system principal, closing the observable side channel.

Key insight
The single mistake was granting an exception for the initial-document case, which let a page-triggered window.open navigation surface an error page and thus leak handler-installed state; the fix removes the aIsInitialDocument condition so only true browser-initiated (system-principal) loads can produce the error page.

Attack Path

  1. Open a popup Attacker script calls window.open to create a new BrowsingContext that is still displaying its initial document.
  2. Navigate to a protocol URI The script directs that popup to a custom-scheme URI backed by an external protocol handler.
  3. Trigger the initial-document branch In FilterStatusForErrorPage the NS_ERROR_UNKNOWN_PROTOCOL case takes the aIsInitialDocument exception, so the load’s outcome depends on whether a handler is registered.
  4. Observe the difference The attacker inspects the popup’s resulting state (error page shown versus silently closed/succeeded) to distinguish installed from missing handlers.
  5. Fingerprint installed apps Repeating across many scheme handlers lets the attacker enumerate which handler-backed applications are installed on the victim’s system.

Impact Assessment

An attacker running untrusted web content in the content process gains a low-severity information disclosure: the ability to detect whether specific protocol handlers, and by extension their associated applications, are installed. The only precondition is that the victim visits an attacker-controlled page that can open popups via window.open; no user interaction beyond the visit is required, and no memory safety or code-execution capability is obtained.

Changed Functions

FunctionChangeNotes
if
docshell/base/nsDocShell.cpp
modified

Files Changed

  • docshell/base/nsDocShell.cpp
  • docshell/base/nsDocShell.h
  • netwerk/ipc/DocumentLoadListener.cpp

Audit Directions

  • Initial-document exceptions
    Review other security decisions that special-case IsInitialDocument, since freshly opened window.open popups routinely occupy that state and can be steered by page script.
  • Error-page divergence
    Look for any load-failure handling whose visible outcome differs based on environment state, as such branches can become side channels revealing installed handlers or resources.
  • Principal-gated suppression
    Audit paths that suppress or surface behavior based on IsSystemPrincipal, ensuring additional conditions do not weaken the intended browser-versus-page distinction.
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 14d7b59aa9c..21edd991239 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -6128,7 +6128,7 @@ already_AddRefed<nsIURI> nsDocShell::AttemptURIFixup(
 
 nsresult nsDocShell::FilterStatusForErrorPage(
     nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType,
-    bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument,
+    bool aIsTopFrame, bool aUseErrorPages,
     bool* aSkippedUnknownProtocolNavigation) {
   // Errors to be shown only on top-level frames
   if ((aStatus == NS_ERROR_UNKNOWN_HOST ||
@@ -6173,17 +6173,10 @@ nsresult nsDocShell::FilterStatusForErrorPage(
 
   if (aStatus == NS_ERROR_UNKNOWN_PROTOCOL) {
     // For unknown protocols we only display an error if the load is triggered
-    // by the browser itself, or we're replacing the initial document (and
-    // nothing else). Showing the error for page-triggered navigations causes
-    // annoying behavior for users, see bug 1528305.
-    //
-    // We could, maybe, try to detect if this is in response to some user
-    // interaction (like clicking a link, or something else) and maybe show
-    // the error page in that case. But this allows for ctrl+clicking and such
-    // to see the error page.
+    // by the browser itself. Showing the error for page-triggered navigations
+    // causes annoying behavior for users, see bug 1528305.
     nsCOMPtr<nsILoadInfo> info = aChannel->LoadInfo();
-    if (!info->TriggeringPrincipal()->IsSystemPrincipal() &&
-        !aIsInitialDocument) {
+    if (!info->TriggeringPrincipal()->IsSystemPrincipal()) {
       if (aSkippedUnknownProtocolNavigation) {
         *aSkippedUnknownProtocolNavigation = true;
       }
@@ -6333,12 +6326,9 @@ nsresult nsDocShell::EndPageLoad(nsIWebProgress* aProgress,
                                 aStatus == NS_ERROR_CONTENT_BLOCKED);
     UnblockEmbedderLoadEventForFailure(fireFrameErrorEvent);
 
-    bool isInitialDocument =
-        !GetExtantDocument() || GetExtantDocument()->IsInitialDocument();
     bool skippedUnknownProtocolNavigation = false;
     aStatus = FilterStatusForErrorPage(aStatus, aChannel, mLoadType, isTopFrame,
                                        mBrowsingContext->GetUseErrorPages(),
-                                       isInitialDocument,
                                        &skippedUnknownProtocolNavigation);
     hadErrorStatus = true;
     if (NS_FAILED(aStatus)) {
diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
index 0ea84df4dde..efc85bf17dc 100644
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -457,7 +457,7 @@ class nsDocShell final : public nsDocLoader,
   // navigation.
   static nsresult FilterStatusForErrorPage(
       nsresult aStatus, nsIChannel* aChannel, uint32_t aLoadType,
-      bool aIsTopFrame, bool aUseErrorPages, bool aIsInitialDocument,
+      bool aIsTopFrame, bool aUseErrorPages,
       bool* aSkippedUnknownProtocolNavigation = nullptr);
 
   // Notify consumers of a search being loaded through the observer service:
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
index ef946929c9b..630a033659c 100644
--- a/netwerk/ipc/DocumentLoadListener.cpp
+++ b/netwerk/ipc/DocumentLoadListener.cpp
@@ -177,10 +177,11 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext,
 
 // Construct a LoadInfo object to use when creating the internal channel for an
 // Object/Embed load.
-static auto CreateObjectLoadInfo(
-    nsDocShellLoadState* aLoadState, uint64_t aInnerWindowId,
-    nsContentPolicyType aContentPolicyType,
-    uint32_t aSandboxFlags) -> already_AddRefed<LoadInfo> {
+static auto CreateObjectLoadInfo(nsDocShellLoadState* aLoadState,
+                                 uint64_t aInnerWindowId,
+                                 nsContentPolicyType aContentPolicyType,
+                                 uint32_t aSandboxFlags)
+    -> already_AddRefed<LoadInfo> {
   RefPtr<WindowGlobalParent> wgp =
       WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
   MOZ_RELEASE_ASSERT(wgp);
@@ -1022,8 +1023,8 @@ auto DocumentLoadListener::OpenObject(
     uint64_t aInnerWindowId, nsLoadFlags aLoadFlags,
     nsContentPolicyType aContentPolicyType, bool aUrgentStart,
     dom::ContentParent* aContentParent,
-    ObjectUpgradeHandler* aObjectUpgradeHandler,
-    nsresult* aRv) -> RefPtr<OpenPromise> {
+    ObjectUpgradeHandler* aObjectUpgradeHandler, nsresult* aRv)
+    -> RefPtr<OpenPromise> {
   LOG(("DocumentLoadListener [%p] OpenObject [uri=%s]", this,
        aLoadState->URI()->GetSpecOrDefault().get()));
 
@@ -1230,9 +1231,10 @@ void DocumentLoadListener::CleanupParentLoadAttempt(uint64_t aLoadIdent) {
   registrar->DeregisterChannels(aLoadIdent);
 }
 
-auto DocumentLoadListener::ClaimParentLoad(
-    DocumentLoadListener** aListener, uint64_t aLoadIdent,
-    Maybe<uint64_t> aChannelId) -> RefPtr<OpenPromise> {
+auto DocumentLoadListener::ClaimParentLoad(DocumentLoadListener** aListener,
+                                           uint64_t aLoadIdent,
+                                           Maybe<uint64_t> aChannelId)
+    -> RefPtr<OpenPromise> {
   nsCOMPtr<nsIRedirectChannelRegistrar> registrar =
       RedirectChannelRegistrar::GetOrCreate();
 
@@ -2364,15 +2366,9 @@ bool DocumentLoadListener::DocShellWillDisplayContent(nsresult aStatus) {
 
   auto* loadingContext = GetLoadingBrowsingContext();
 
-  bool isInitialDocument = true;
-  if (WindowGlobalParent* currentWindow =
-          loadingContext->GetCurrentWindowGlobal()) {
-    isInitialDocument = currentWindow->IsInitialDocument();
-  }
-
   nsresult rv = nsDocShell::FilterStatusForErrorPage(
       aStatus, mChannel, mLoadStateLoadType, loadingContext->IsTop(),
-      loadingContext->GetUseErrorPages(), isInitialDocument, nullptr);
+      loadingContext->GetUseErrorPages(), nullptr);
 
   if (NS_SUCCEEDED(rv)) {
     MOZ_LOG(gProcessIsolationLog, LogLevel::Verbose,
Loading diff…