Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the DOM: Navigation component
ComponentDOM
Bug ClassLogic Error
Tracker2069258
Fix commit64bcee60617c (firefox) +125/-27
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
for
browser/modules/test/unit/test_E10SUtils_nested_URIs.js
modified

Files Changed

  • browser/modules/test/unit/test_E10SUtils_nested_URIs.js
  • docshell/base/nsDocShellLoadState.cpp
  • dom/ipc/ProcessIsolation.cpp
  • dom/ipc/ProcessIsolation.h
  • toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp
index f055c9c70c7..73d38cea38f 100644
--- a/docshell/base/nsDocShellLoadState.cpp
+++ b/docshell/base/nsDocShellLoadState.cpp
@@ -68,6 +68,11 @@ bool ContentTriggeredURILoadIsAllowed(nsIURI* aURI,
            ContentTriggeredURILoadIsAllowed(innerURI, aEffectiveRemoteType);
   }
 
+  // about:reader is process-allocated based on the "url" parameter.
+  if (nsCOMPtr<nsIURI> readerURI = GetAboutReaderURL(aURI)) {
+    return ContentTriggeredURILoadIsAllowed(readerURI, aEffectiveRemoteType);
+  }
+
   // A null principal is the least privileged principal there is, so any URI it
   // is allowed to link to may be loaded from any content process.
   nsCOMPtr<nsIPrincipal> genericNullPrincipal = NullPrincipal::Create({});
diff --git a/dom/ipc/ProcessIsolation.cpp b/dom/ipc/ProcessIsolation.cpp
index 3a178cc4c97..a1b725eafb3 100644
--- a/dom/ipc/ProcessIsolation.cpp
+++ b/dom/ipc/ProcessIsolation.cpp
@@ -432,33 +432,6 @@ static nsAutoCString OriginAttributesString(const OriginAttributes& aAttrs) {
   return originSuffix;
 }
 
-/**
- * Given an about:reader URI, extract the "url" query parameter, and use it to
- * construct a principal which should be used for process selection.
- */
-static already_AddRefed<nsIURI> GetAboutReaderURL(nsIURI* aURI) {
-#ifdef DEBUG
-  MOZ_ASSERT(aURI->SchemeIs("about"));
-  nsAutoCString path;
-  MOZ_ALWAYS_SUCCEEDS(NS_GetAboutModuleName(aURI, path));
-  MOZ_ASSERT(path == "reader"_ns);
-#endif
-
-  nsAutoCString query;
-  MOZ_ALWAYS_SUCCEEDS(aURI->GetQuery(query));
-
-  // Extract the "url" parameter from the `about:reader`'s query parameters,
-  // and recover a content principal from it.
-  nsAutoCString readerSpec;
-  if (URLParams::Extract(query, "url"_ns, readerSpec)) {
-    nsCOMPtr<nsIURI> readerUri;
-    if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(readerUri), readerSpec))) {
-      return readerUri.forget();
-    }
-  }
-  return nullptr;
-}
-
 static already_AddRefed<BasePrincipal> GetAboutReaderURLPrincipal(
     nsIURI* aURI, const OriginAttributes& aAttrs) {
   if (nsCOMPtr<nsIURI> readerUri = GetAboutReaderURL(aURI)) {
@@ -600,6 +573,34 @@ static Result<RemoteType, nsresult> SpecialBehaviorRemoteType(
 
 }  // namespace
 
+already_AddRefed<nsIURI> GetAboutReaderURL(nsIURI* aURI) {
+  if (!aURI->SchemeIs("about")) {
+    return nullptr;
+  }
+  nsAutoCString path;
+  if (NS_FAILED(NS_GetAboutModuleName(aURI, path)) || path != "reader"_ns) {
+    return nullptr;
+  }
+
+  nsAutoCString query;
+  MOZ_ALWAYS_SUCCEEDS(aURI->GetQuery(query));
+
+  // Extract the "url" parameter from the `about:reader`'s query parameters,
+  // and recover a content principal from it.
+  nsAutoCString readerSpec;
+  if (URLParams::Extract(query, "url"_ns, readerSpec)) {
+    nsCOMPtr<nsIURI> readerUri;
+    if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(readerUri), readerSpec)) &&
+        // about:reader only loads http(s) and file documents; see
+        // AboutReader.sys.mjs.
+        (readerUri->SchemeIs("http") || readerUri->SchemeIs("https") ||
+         readerUri->SchemeIs("file"))) {
+      return readerUri.forget();
+    }
+  }
+  return nullptr;
+}
+
 Result<NavigationIsolationOptions, nsresult> IsolationOptionsForNavigation(
     CanonicalBrowsingContext* aTopBC, WindowGlobalParent* aParentWindow,
     nsIURI* aChannelCreationURI, nsIChannel* aChannel,
@@ -1631,6 +1632,11 @@ bool ValidatePrincipalCouldPotentiallyBeLoadedBy(
         // Allow about:reader to load anywhere, as it is process-allocated based
         // on the content it displays, and which content is being displayed is
         // unfortunately not part of the principal.
+        //
+        // NOTE: This means this check cannot constrain where an about:reader
+        // document ends up. The "url" parameter is restricted in
+        // GetAboutReaderURL and validated against the requesting process in
+        // ContentTriggeredURILoadIsAllowed.
         return true;
       case IsolationBehavior::Extension:
         return aRemoteType.IsExtension();
diff --git a/dom/ipc/ProcessIsolation.h b/dom/ipc/ProcessIsolation.h
index a7ca1054b1d..67c6588d243 100644
--- a/dom/ipc/ProcessIsolation.h
+++ b/dom/ipc/ProcessIsolation.h
@@ -90,6 +90,15 @@ Result<RemoteType, nsresult> PredictRemoteTypeForURI(
     nsIURI* aURI, const OriginAttributes& aOriginAttributes,
     const RemoteType& aPreferredRemoteType, bool aUseRemoteSubframes);
 
+/**
+ * If `aURI` is an `about:reader` URI whose "url" query parameter names a
+ * document about:reader will actually load, return that URL, otherwise return
+ * `nullptr`.
+ *
+ * Safe to call with any URI.
+ */
+already_AddRefed<nsIURI> GetAboutReaderURL(nsIURI* aURI);
+
 /**
  * Adds a `highValue` permission to the permissions database, and make loads of
  * that origin isolated.
diff --git a/browser/modules/test/unit/test_E10SUtils_nested_URIs.js b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
index 8fe4528fad5..098f6f2edfc 100644
--- a/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
+++ b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
@@ -43,6 +43,29 @@ var TEST_CASES = [
   },
 ];
 
+// The "url" parameter of an about:reader URI steers process selection like a
+// nested URI does, but about:reader only ever loads http(s) and file
+// documents, so for any other scheme the parameter must be ignored.
+var READER_HONOURS_URL = [
+  {
+    readerURL: "about:reader?url=http%3A%2F%2Fsome.site",
+    plainURL: "http://some.site",
+  },
+  {
+    readerURL: "about:reader?url=file%3A%2F%2F%2Fsome.file",
+    plainURL: "file:///some.file",
+  },
+];
+
+var READER_IGNORES_URL = [
+  "about:reader?url=about%3Aconfig",
+  "about:reader?url=about%3Alogins",
+  "about:reader?url=chrome%3A%2F%2Fglobal%2Fcontent%2Freader%2FaboutReader.html",
+  "about:reader?url=resource%3A%2F%2Fgre%2Fmodules%2FE10SUtils.sys.mjs",
+  "about:reader?url=moz-extension%3A%2F%2F6c56e6dd-e2c1-4d2f-9c9d-6d0e6f0d3b62%2Fx.html",
+  "about:reader?url=jar%3Afile%3A%2F%2F%2Fsome.file!%2F",
+];
+
 function run_test() {
   for (let testCase of TEST_CASES) {
     for (let preferredRemoteType of TEST_PREFERRED_REMOTE_TYPES) {
@@ -84,4 +107,48 @@ function run_test() {
       }
     }
   }
+
+  for (let preferredRemoteType of TEST_PREFERRED_REMOTE_TYPES) {
+    for (let useRemoteSubframes of TEST_USE_REMOTE_SUBFRAMES) {
+      let options = {
+        useRemoteTabs: true,
+        useRemoteSubframes,
+        preferredRemoteType,
+      };
+      let suffix =
+        ` with preferred remote type: ${preferredRemoteType}` +
+        ` and remote subframes: ${useRemoteSubframes}`;
+
+      for (let { readerURL, plainURL } of READER_HONOURS_URL) {
+        equal(
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(readerURL),
+            options
+          ),
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(plainURL),
+            options
+          ),
+          `Check that ${readerURL} loads in same remote type as ${plainURL}` +
+            suffix
+        );
+      }
+
+      let bareReaderRemoteType = ChromeUtils.predictRemoteTypeForURI(
+        Services.io.newURI("about:reader"),
+        options
+      );
+      for (let readerURL of READER_IGNORES_URL) {
+        equal(
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(readerURL),
+            options
+          ),
+          bareReaderRemoteType,
+          `Check that ${readerURL} loads in same remote type as about:reader` +
+            suffix
+        );
+      }
+    }
+  }
 }
diff --git a/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
index 118e4bb23f8..d8ae36c0e26 100644
--- a/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
+++ b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
@@ -24,11 +24,22 @@ add_task(async function test_readermode_available_for_local_files() {
         "Reader mode button is present on a reader-able page"
       );
 
+      let fileRemoteType = browser.remoteType;
+
       // Switch page into reader mode.
       let promiseTabLoad = BrowserTestUtils.browserLoaded(browser);
       readerButton.click();
       await promiseTabLoad;
 
+      // Entering reader mode is a content-triggered navigation to
+      // about:reader?url=file://..., so it must both be allowed from and stay
+      // within the article's own content process.
+      is(
+        gBrowser.selectedBrowser.remoteType,
+        fileRemoteType,
+        "Reader mode on a local file stays in the file content process"
+      );
+
       let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
       ok(
         readerUrl.startsWith("about:reader"),
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/modules/test/unit/test_E10SUtils_nested_URIs.js b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
index 8fe4528fad5..098f6f2edfc 100644
--- a/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
+++ b/browser/modules/test/unit/test_E10SUtils_nested_URIs.js
@@ -43,6 +43,29 @@ var TEST_CASES = [
   },
 ];
 
+// The "url" parameter of an about:reader URI steers process selection like a
+// nested URI does, but about:reader only ever loads http(s) and file
+// documents, so for any other scheme the parameter must be ignored.
+var READER_HONOURS_URL = [
+  {
+    readerURL: "about:reader?url=http%3A%2F%2Fsome.site",
+    plainURL: "http://some.site",
+  },
+  {
+    readerURL: "about:reader?url=file%3A%2F%2F%2Fsome.file",
+    plainURL: "file:///some.file",
+  },
+];
+
+var READER_IGNORES_URL = [
+  "about:reader?url=about%3Aconfig",
+  "about:reader?url=about%3Alogins",
+  "about:reader?url=chrome%3A%2F%2Fglobal%2Fcontent%2Freader%2FaboutReader.html",
+  "about:reader?url=resource%3A%2F%2Fgre%2Fmodules%2FE10SUtils.sys.mjs",
+  "about:reader?url=moz-extension%3A%2F%2F6c56e6dd-e2c1-4d2f-9c9d-6d0e6f0d3b62%2Fx.html",
+  "about:reader?url=jar%3Afile%3A%2F%2F%2Fsome.file!%2F",
+];
+
 function run_test() {
   for (let testCase of TEST_CASES) {
     for (let preferredRemoteType of TEST_PREFERRED_REMOTE_TYPES) {
@@ -84,4 +107,48 @@ function run_test() {
       }
     }
   }
+
+  for (let preferredRemoteType of TEST_PREFERRED_REMOTE_TYPES) {
+    for (let useRemoteSubframes of TEST_USE_REMOTE_SUBFRAMES) {
+      let options = {
+        useRemoteTabs: true,
+        useRemoteSubframes,
+        preferredRemoteType,
+      };
+      let suffix =
+        ` with preferred remote type: ${preferredRemoteType}` +
+        ` and remote subframes: ${useRemoteSubframes}`;
+
+      for (let { readerURL, plainURL } of READER_HONOURS_URL) {
+        equal(
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(readerURL),
+            options
+          ),
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(plainURL),
+            options
+          ),
+          `Check that ${readerURL} loads in same remote type as ${plainURL}` +
+            suffix
+        );
+      }
+
+      let bareReaderRemoteType = ChromeUtils.predictRemoteTypeForURI(
+        Services.io.newURI("about:reader"),
+        options
+      );
+      for (let readerURL of READER_IGNORES_URL) {
+        equal(
+          ChromeUtils.predictRemoteTypeForURI(
+            Services.io.newURI(readerURL),
+            options
+          ),
+          bareReaderRemoteType,
+          `Check that ${readerURL} loads in same remote type as about:reader` +
+            suffix
+        );
+      }
+    }
+  }
 }
diff --git a/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
index 118e4bb23f8..d8ae36c0e26 100644
--- a/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
+++ b/toolkit/components/reader/tests/browser/browser_localfile_readerMode.js
@@ -24,11 +24,22 @@ add_task(async function test_readermode_available_for_local_files() {
         "Reader mode button is present on a reader-able page"
       );
 
+      let fileRemoteType = browser.remoteType;
+
       // Switch page into reader mode.
       let promiseTabLoad = BrowserTestUtils.browserLoaded(browser);
       readerButton.click();
       await promiseTabLoad;
 
+      // Entering reader mode is a content-triggered navigation to
+      // about:reader?url=file://..., so it must both be allowed from and stay
+      // within the article's own content process.
+      is(
+        gBrowser.selectedBrowser.remoteType,
+        fileRemoteType,
+        "Reader mode on a local file stays in the file content process"
+      );
+
       let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
       ok(
         readerUrl.startsWith("about:reader"),
Loading diff…