Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMitigation bypass in the DOM: Service Workers component
ComponentDOM
Bug ClassLogic Error
Tracker2041864
Fix commit9f80d0a7fd59 (firefox) +87/-29
CISA KEVNot listed
CreditedYaqoub Aldurayhim
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
if
dom/workers/ScriptLoader.cpp
modified
if
dom/workers/WorkerLoadInfo.cpp
modified
add_task
dom/workers/test/browser_system_worker_page_icon_rejected.js
modified

Files Changed

  • dom/workers/ScriptLoader.cpp
  • dom/workers/WorkerLoadInfo.cpp
  • dom/workers/test/browser.toml
  • dom/workers/test/browser_system_worker_page_icon_rejected.js
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…