Firefox · DOM
CVE-2026-109756
Logic Error in DOM
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forbrowser/modules/test/unit/test_E10SUtils_nested_URIs.js |
modified |
Files Changed
browser/modules/test/unit/test_E10SUtils_nested_URIs.jsdocshell/base/nsDocShellLoadState.cppdom/ipc/ProcessIsolation.cppdom/ipc/ProcessIsolation.htoolkit/components/reader/tests/browser/browser_localfile_readerMode.js
Patch
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…
References
On This Page