Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionMitigation bypass in the DOM: Service Workers component
ComponentDOM
Bug ClassLogic Error
Tracker2044063
Fix commit4f7060aa7884 (firefox) +40/-2
CISA KEVNot listed
Creditedbug2own
Disclosed2026-07-21

Background

Service Worker
a script the browser runs in the background to intercept and control network requests for a page’s scope.
`importScripts()`
a classic (non-module) worker API that synchronously fetches and executes additional scripts into the worker’s global scope.
JavaScript MIME type
a content type such as text/javascript that nsContentUtils::IsJavascriptMIMEType recognizes as executable script.
JSON module import
an ES module import of the form import data from "./x.json" with { type: "json" }, the only context where a JSON MIME type is legitimately loadable.

Root Cause Analysis

The MIME-type validation in CompareNetwork::OnStreamComplete (service worker script comparison) and in NetworkLoadHandler::PrepareForRequest (worker script loading) accepted a response as a valid script whenever nsContentUtils::IsJsonMimeType(mimeTypeUTF16) returned true, regardless of whether the load was a top-level script, a classic importScripts() call, or an actual JSON module import. This violated the invariant that only genuine JSON module imports may carry a JSON MIME type; a top-level service worker script or a classic importScripts() resource served as application/json, text/json, or application/manifest+json but containing valid JavaScript would be executed.

The fix in CompareNetwork::OnStreamComplete gates the JSON allowance on !mIsMainScript, rejecting a top-level script served as JSON.

The fix in NetworkLoadHandler::PrepareForRequest narrows the JSON allowance to loads that are simultaneously non-top-level, a module request (loadContext->mRequest->IsModuleRequest()), and of module type JS::ModuleType::JSON, so classic importScripts() and the top-level worker script can no longer smuggle in JSON-typed script.

The fix works because it re-couples the JSON MIME allowance to the exact load context (JS::ModuleType::JSON) that the JSON exception was ever meant to cover.

Key insight
The single core mistake was treating a JSON MIME type as universally acceptable for worker/service-worker script loads instead of restricting it to actual JSON module imports; the fix reintroduces the missing context checks (!mIsMainScript, and IsModuleRequest() plus mModuleType == JS::ModuleType::JSON) that confine JSON to its legitimate use.

Attack Path

  1. Host a JSON-typed script An attacker or a site that only controls a JSON-serving endpoint provides a resource whose body is valid JavaScript but whose Content-Type is application/json, text/json, or application/manifest+json.
  2. Register or import it The resource is used as a top-level service worker script, or is pulled in via classic importScripts(), where a JavaScript MIME type would normally be required.
  3. Bypass the MIME check The old IsJsonMimeType allowance passes the resource through CompareNetwork::OnStreamComplete / NetworkLoadHandler::PrepareForRequest without the JavaScript-MIME restriction.
  4. Execute the script The JSON-typed body is run as worker/service-worker code, defeating the MIME-type mitigation intended to prevent scripts from being sourced from non-script content types.

Impact Assessment

An attacker gains the ability to load and execute worker or service worker JavaScript from responses served with a JSON MIME type, bypassing a MIME-type-based mitigation that is meant to stop content-type confusion. This runs in the worker / service worker context of the affected origin. The precondition is a response body containing valid JavaScript served with a JSON content type and used as a top-level script or via importScripts(); the issue is rated low severity as a mitigation bypass rather than a direct memory-safety or cross-origin compromise.

Changed Functions

FunctionChangeNotes
for
testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
modified
promise_test
testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
modified

Files Changed

  • dom/serviceworkers/ServiceWorkerScriptCache.cpp
  • dom/workers/loader/NetworkLoadHandler.cpp
  • testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
  • testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js

Audit Directions

  • MIME allowlist without context
    Flag any script-load path that accepts a broad content-type predicate like IsJsonMimeType without also checking whether the load is top-level, classic, or a specific module type.
  • Module-type coupling
    Verify that JSON (and wasm) MIME exceptions are always gated on the concrete JS::ModuleType and IsModuleRequest() state rather than being applied to all worker loads.
  • Dual enforcement points
    Confirm both the registration/comparison path (ServiceWorkerScriptCache.cpp) and the network load path (NetworkLoadHandler.cpp) apply the same MIME restrictions, since a check missing in either enables the bypass.
diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.cpp b/dom/serviceworkers/ServiceWorkerScriptCache.cpp
index 6a1211f96de..2e80b226620 100644
--- a/dom/serviceworkers/ServiceWorkerScriptCache.cpp
+++ b/dom/serviceworkers/ServiceWorkerScriptCache.cpp
@@ -1087,9 +1087,12 @@ CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader,
     }
 
     auto mimeTypeUTF16 = NS_ConvertUTF8toUTF16(mimeType);
+    // The top-level service worker script must be served with a JavaScript
+    // MIME type. JSON is only permitted for non-top-level (imported) modules,
+    // such as `import data from "./x.json" with { type: "json" }`.
     if (mimeTypeUTF16.IsEmpty() ||
         !(nsContentUtils::IsJavascriptMIMEType(mimeTypeUTF16) ||
-          nsContentUtils::IsJsonMimeType(mimeTypeUTF16))) {
+          (!mIsMainScript && nsContentUtils::IsJsonMimeType(mimeTypeUTF16)))) {
       ServiceWorkerManager::LocalizeAndReportToAllClients(
           mRegistration->Scope(), "ServiceWorkerRegisterMimeTypeError2",
           nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()),
diff --git a/dom/workers/loader/NetworkLoadHandler.cpp b/dom/workers/loader/NetworkLoadHandler.cpp
index 75a46add9dd..7d384eb01d9 100644
--- a/dom/workers/loader/NetworkLoadHandler.cpp
+++ b/dom/workers/loader/NetworkLoadHandler.cpp
@@ -353,8 +353,12 @@ nsresult NetworkLoadHandler::PrepareForRequest(nsIRequest* aRequest) {
 
     auto mimeTypeUTF16 = NS_ConvertUTF8toUTF16(mimeType);
     if (!nsContentUtils::IsJavascriptMIMEType(mimeTypeUTF16)) {
-      // JSON is allowed as a non-toplevel.
+      // JSON is only allowed for non-toplevel JSON module imports, not for
+      // classic importScripts() or the top-level worker script.
       if (!((!loadContext->IsTopLevel() &&
+             loadContext->mRequest->IsModuleRequest() &&
+             loadContext->mRequest->AsModuleRequest()->mModuleType ==
+                 JS::ModuleType::JSON &&
              nsContentUtils::IsJsonMimeType(mimeTypeUTF16))
 #ifdef NIGHTLY_BUILD
             // Allow wasm modules.
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
index d4f1f3e26d8..7658eeace69 100644
--- a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
@@ -1,6 +1,11 @@
 const badMimeTypes = [
   null,  // no MIME type
   'text/plain',
+  // JSON is only valid for JSON module imports, never for classic
+  // importScripts(), even when the body is valid JavaScript.
+  'application/json',
+  'text/json',
+  'application/manifest+json',
 ];
 
 const validMimeTypes = [
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
index 037e6c0fde2..7a64cd91e41 100644
--- a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
@@ -21,6 +21,32 @@ function registration_tests_mime_types(register_method) {
           'Registration of plain text script should fail.');
     }, 'Registering script with bad MIME type');
 
+  // The top-level service worker script must have a JavaScript MIME type. A
+  // JSON MIME type is only valid for imported (non-top-level) modules, so
+  // registering a top-level script served as JSON must fail even when the body
+  // happens to be valid JavaScript.
+  const jsonMimeTypes = [
+    'application/json',
+    'text/json',
+    'application/manifest+json',
+  ];
+
+  for (const jsonMimeType of jsonMimeTypes) {
+    promise_test(function(t) {
+        // Encode the MIME type so characters such as '+' survive the query
+        // string instead of being decoded to a space by the server.
+        var script =
+            `resources/mime-type-worker.py?mime=${encodeURIComponent(jsonMimeType)}`;
+        // Use a scope unique to each MIME type so the registrations don't
+        // interfere with one another.
+        var scope = `resources/scope/json-mime-type-worker/${jsonMimeType}`;
+        return promise_rejects_dom(t,
+            'SecurityError',
+            register_method(script, {scope: scope}),
+            'Registration of JSON MIME type script should fail.');
+      }, `Registering script with JSON MIME type ${jsonMimeType}`);
+  }
+
   /**
    * ServiceWorkerContainer.register() should throw a TypeError, according to
    * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
index d4f1f3e26d8..7658eeace69 100644
--- a/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js
@@ -1,6 +1,11 @@
 const badMimeTypes = [
   null,  // no MIME type
   'text/plain',
+  // JSON is only valid for JSON module imports, never for classic
+  // importScripts(), even when the body is valid JavaScript.
+  'application/json',
+  'text/json',
+  'application/manifest+json',
 ];
 
 const validMimeTypes = [
diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
index 037e6c0fde2..7a64cd91e41 100644
--- a/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
+++ b/testing/web-platform/tests/service-workers/service-worker/resources/registration-tests-mime-types.js
@@ -21,6 +21,32 @@ function registration_tests_mime_types(register_method) {
           'Registration of plain text script should fail.');
     }, 'Registering script with bad MIME type');
 
+  // The top-level service worker script must have a JavaScript MIME type. A
+  // JSON MIME type is only valid for imported (non-top-level) modules, so
+  // registering a top-level script served as JSON must fail even when the body
+  // happens to be valid JavaScript.
+  const jsonMimeTypes = [
+    'application/json',
+    'text/json',
+    'application/manifest+json',
+  ];
+
+  for (const jsonMimeType of jsonMimeTypes) {
+    promise_test(function(t) {
+        // Encode the MIME type so characters such as '+' survive the query
+        // string instead of being decoded to a space by the server.
+        var script =
+            `resources/mime-type-worker.py?mime=${encodeURIComponent(jsonMimeType)}`;
+        // Use a scope unique to each MIME type so the registrations don't
+        // interfere with one another.
+        var scope = `resources/scope/json-mime-type-worker/${jsonMimeType}`;
+        return promise_rejects_dom(t,
+            'SecurityError',
+            register_method(script, {scope: scope}),
+            'Registration of JSON MIME type script should fail.');
+      }, `Registering script with JSON MIME type ${jsonMimeType}`);
+  }
+
   /**
    * ServiceWorkerContainer.register() should throw a TypeError, according to
    * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts
Loading diff…