Firefox · DOM
CVE-2026-16382
Logic Error in DOM
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/workers/ScriptLoader.cpp |
modified | |
ifdom/workers/WorkerLoadInfo.cpp |
modified | |
add_taskdom/workers/test/browser_system_worker_page_icon_rejected.js |
modified |
Files Changed
dom/workers/ScriptLoader.cppdom/workers/WorkerLoadInfo.cppdom/workers/test/browser.tomldom/workers/test/browser_system_worker_page_icon_rejected.js
Patch
diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp
index c6d587f0f8c..47590f39ec1 100644
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -42,6 +42,7 @@
#include "nsComponentManagerUtils.h"
#include "nsContentPolicyUtils.h"
#include "nsContentSecurityManager.h"
+#include "nsContentSecurityUtils.h"
#include "nsContentUtils.h"
#include "nsDocShellCID.h"
#include "nsError.h"
@@ -57,7 +58,6 @@
#include "nsIOutputStream.h"
#include "nsIPipe.h"
#include "nsIPrincipal.h"
-#include "nsIProtocolHandler.h"
#include "nsIScriptError.h"
#include "nsIScriptSecurityManager.h"
#include "nsIStreamListenerTee.h"
@@ -364,15 +364,11 @@ nsresult GetCommonSecFlags(bool aIsMainScript, nsIURI* uri,
}
if (aWorkerScriptType == DebuggerScript) {
- // A DebuggerScript needs to be a local resource like chrome: or resource:
- bool isUIResource = false;
- nsresult rv = NS_URIChainHasFlags(
- uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUIResource);
- if (NS_WARN_IF(NS_FAILED(rv))) {
- return rv;
- }
-
- if (!isUIResource) {
+ // A DebuggerScript needs to be a chrome script resource like chrome: or
+ // resource:. We restrict it to those trusted schemes rather than the broad
+ // URI_IS_UI_RESOURCE flag, which image/UI data protocols (page-icon:,
+ // moz-icon:, ...) also carry and must never be loaded as worker scripts.
+ if (!nsContentSecurityUtils::IsTrustedScheme(uri)) {
return NS_ERROR_DOM_SECURITY_ERR;
}
diff --git a/dom/workers/WorkerLoadInfo.cpp b/dom/workers/WorkerLoadInfo.cpp
index 99969d2995f..6d448f1021a 100644
--- a/dom/workers/WorkerLoadInfo.cpp
+++ b/dom/workers/WorkerLoadInfo.cpp
@@ -15,12 +15,12 @@
#include "mozilla/dom/nsCSPUtils.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsContentSecurityUtils.h"
#include "nsContentUtils.h"
#include "nsIBrowserChild.h"
#include "nsIContentSecurityPolicy.h"
#include "nsICookieJarSettings.h"
#include "nsINetworkInterceptController.h"
-#include "nsIProtocolHandler.h"
#include "nsIReferrerInfo.h"
#include "nsNetUtil.h"
#include "nsScriptSecurityManager.h"
@@ -195,17 +195,14 @@ nsresult WorkerLoadInfo::GetPrincipalsAndLoadGroupFromChannel(
rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalURI));
NS_ENSURE_SUCCESS(rv, rv);
- // See if this is a resource URI. Since JSMs usually come from
- // resource:// URIs we're currently considering all URIs with the
- // URI_IS_UI_RESOURCE flag as valid for creating privileged workers.
- bool isResource;
- rv = NS_URIChainHasFlags(finalURI, nsIProtocolHandler::URI_IS_UI_RESOURCE,
- &isResource);
- NS_ENSURE_SUCCESS(rv, rv);
-
- if (isResource) {
- // Assign the system principal to the resource:// worker only if it
- // was loaded from code using the system principal.
+ // Privileged workers' scripts come from script-bearing chrome schemes
+ // such as resource:// (where JSMs live). We restrict them to those
+ // trusted schemes rather than the broad URI_IS_UI_RESOURCE flag, which
+ // image/UI data protocols (page-icon:, moz-icon:, ...) also carry and
+ // must never be loaded as worker scripts.
+ if (nsContentSecurityUtils::IsTrustedScheme(finalURI)) {
+ // Assign the system principal to the worker only if it was loaded from
+ // code using the system principal.
channelPrincipal = mLoadingPrincipal;
channelPartitionedPrincipal = mLoadingPrincipal;
} else {
@@ -293,18 +290,14 @@ bool WorkerLoadInfo::PrincipalURIMatchesScriptURL() {
nsresult rv = mBaseURI->GetScheme(scheme);
NS_ENSURE_SUCCESS(rv, false);
- // A system principal must either be a blob URL or a resource JSM.
+ // A system principal must either be a blob URL or a chrome script resource
+ // (e.g. a resource:// JSM).
if (mPrincipal->IsSystemPrincipal()) {
if (scheme == "blob"_ns) {
return true;
}
- bool isResource = false;
- nsresult rv = NS_URIChainHasFlags(
- mBaseURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isResource);
- NS_ENSURE_SUCCESS(rv, false);
-
- return isResource;
+ return nsContentSecurityUtils::IsTrustedScheme(mBaseURI);
}
// A null principal can occur for a data URL worker script or a blob URL
diff --git a/dom/workers/test/browser.toml b/dom/workers/test/browser.toml
index 102e4aaf636..b19acf8a6da 100644
--- a/dom/workers/test/browser.toml
+++ b/dom/workers/test/browser.toml
@@ -55,6 +55,8 @@ support-files = [
"server_fetch_synthetic.sjs",
]
+["browser_system_worker_page_icon_rejected.js"]
+
["browser_worker_use_counters.js"]
support-files = [
"file_use_counter_worker.html",
diff --git a/dom/workers/test/browser_system_worker_page_icon_rejected.js b/dom/workers/test/browser_system_worker_page_icon_rejected.js
new file mode 100644
index 00000000000..a9f54d730ab
--- /dev/null
+++ b/dom/workers/test/browser_system_worker_page_icon_rejected.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+// These bytes are simultaneously valid JavaScript (the leading block comment
+// hides the markup from the JS parser) and a payload that Places stores
+// verbatim as an "SVG" favicon (it only checks for a "<svg" substring). If a
+// worker script load from page-icon: were *not* blocked, this would execute
+// and post "executed" back. The fix must prevent the load entirely.
+const WORKER_SOURCE = `/*<svg*/
+self.postMessage("executed:" + (typeof ChromeUtils));
+`;
+
+const PAGE_URL = "https://example.com/system-worker-page-icon-test/";
+const FAVICON_URL = "https://example.com/system-worker-page-icon-test/icon.svg";
+const PAGE_ICON_URL = "page-icon:" + PAGE_URL;
+
+add_task(async function test_system_worker_cannot_load_page_icon_script() {
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Seed Places with an SVG favicon whose bytes are valid JavaScript.
+ await PlacesTestUtils.addVisits(PAGE_URL);
+ await PlacesTestUtils.setFaviconForPage(
+ PAGE_URL,
+ FAVICON_URL,
+ "data:image/svg+xml;base64," + btoa(WORKER_SOURCE)
+ );
+
+ // Sanity check: page-icon: returns our attacker-controlled bytes verbatim, so
+ // absent the fix a worker loading this URL would execute them.
+ const favicon = await PlacesTestUtils.getFaviconForPage(PAGE_URL);
+ is(
+ favicon?.rawData &&
+ new TextDecoder().decode(Uint8Array.from(favicon.rawData)),
+ WORKER_SOURCE,
+ "page-icon: stores the attacker-controlled bytes verbatim"
+ );
+
+ // The actual test: a system-principal (Chrome) worker must refuse to load a
+ // page-icon: script. The script load is rejected synchronously during
+ // construction (NS_ERROR_DOM_BAD_URI), so the constructor throws a
+ // SecurityError rather than ever executing the worker.
+ let error;
+ try {
+ new ChromeWorker(PAGE_ICON_URL);
+ } catch (e) {
+ error = e;
+ }
+
+ ok(
+ error,
+ "A system-principal worker must refuse to load a page-icon: script"
+ );
+ ok(
+ DOMException.isInstance(error) && error.name === "SecurityError",
+ "ChromeWorker construction throws SecurityError for a page-icon: script: " +
+ error
+ );
+});
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/dom/workers/test/browser.toml b/dom/workers/test/browser.toml
index 102e4aaf636..b19acf8a6da 100644
--- a/dom/workers/test/browser.toml
+++ b/dom/workers/test/browser.toml
@@ -55,6 +55,8 @@ support-files = [
"server_fetch_synthetic.sjs",
]
+["browser_system_worker_page_icon_rejected.js"]
+
["browser_worker_use_counters.js"]
support-files = [
"file_use_counter_worker.html",
diff --git a/dom/workers/test/browser_system_worker_page_icon_rejected.js b/dom/workers/test/browser_system_worker_page_icon_rejected.js
new file mode 100644
index 00000000000..a9f54d730ab
--- /dev/null
+++ b/dom/workers/test/browser_system_worker_page_icon_rejected.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+// These bytes are simultaneously valid JavaScript (the leading block comment
+// hides the markup from the JS parser) and a payload that Places stores
+// verbatim as an "SVG" favicon (it only checks for a "<svg" substring). If a
+// worker script load from page-icon: were *not* blocked, this would execute
+// and post "executed" back. The fix must prevent the load entirely.
+const WORKER_SOURCE = `/*<svg*/
+self.postMessage("executed:" + (typeof ChromeUtils));
+`;
+
+const PAGE_URL = "https://example.com/system-worker-page-icon-test/";
+const FAVICON_URL = "https://example.com/system-worker-page-icon-test/icon.svg";
+const PAGE_ICON_URL = "page-icon:" + PAGE_URL;
+
+add_task(async function test_system_worker_cannot_load_page_icon_script() {
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+
+ // Seed Places with an SVG favicon whose bytes are valid JavaScript.
+ await PlacesTestUtils.addVisits(PAGE_URL);
+ await PlacesTestUtils.setFaviconForPage(
+ PAGE_URL,
+ FAVICON_URL,
+ "data:image/svg+xml;base64," + btoa(WORKER_SOURCE)
+ );
+
+ // Sanity check: page-icon: returns our attacker-controlled bytes verbatim, so
+ // absent the fix a worker loading this URL would execute them.
+ const favicon = await PlacesTestUtils.getFaviconForPage(PAGE_URL);
+ is(
+ favicon?.rawData &&
+ new TextDecoder().decode(Uint8Array.from(favicon.rawData)),
+ WORKER_SOURCE,
+ "page-icon: stores the attacker-controlled bytes verbatim"
+ );
+
+ // The actual test: a system-principal (Chrome) worker must refuse to load a
+ // page-icon: script. The script load is rejected synchronously during
+ // construction (NS_ERROR_DOM_BAD_URI), so the constructor throws a
+ // SecurityError rather than ever executing the worker.
+ let error;
+ try {
+ new ChromeWorker(PAGE_ICON_URL);
+ } catch (e) {
+ error = e;
+ }
+
+ ok(
+ error,
+ "A system-principal worker must refuse to load a page-icon: script"
+ );
+ ok(
+ DOMException.isInstance(error) && error.name === "SecurityError",
+ "ChromeWorker construction throws SecurityError for a page-icon: script: " +
+ error
+ );
+});
Loading diff…
References
On This Page