CVE-2024-9398
Overview
Background
- `nsDocShell::FilterStatusForErrorPage`
- The
nsDocShellhelper 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
BrowsingContextholds before any real content loads, reported byIsInitialDocument; a freshly createdwindow.openpopup 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.
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
- Open a popup
Attacker script calls
window.opento create a newBrowsingContextthat is still displaying its initial document. - Navigate to a protocol URI The script directs that popup to a custom-scheme URI backed by an external protocol handler.
- Trigger the initial-document branch
In
FilterStatusForErrorPagetheNS_ERROR_UNKNOWN_PROTOCOLcase takes theaIsInitialDocumentexception, so the load’s outcome depends on whether a handler is registered. - Observe the difference The attacker inspects the popup’s resulting state (error page shown versus silently closed/succeeded) to distinguish installed from missing handlers.
- 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
window.open; no user interaction beyond the visit is required, and no memory safety or code-execution capability is obtained.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdocshell/base/nsDocShell.cpp |
modified |
Files Changed
docshell/base/nsDocShell.cppdocshell/base/nsDocShell.hnetwerk/ipc/DocumentLoadListener.cpp
Audit Directions
- Initial-document exceptionsReview other security decisions that special-case
IsInitialDocument, since freshly openedwindow.openpopups routinely occupy that state and can be steered by page script. - Error-page divergenceLook 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 suppressionAudit paths that suppress or surface behavior based on
IsSystemPrincipal, ensuring additional conditions do not weaken the intended browser-versus-page distinction.
Patch
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,