Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMitigation bypass in the DOM: Networking component
ComponentDOM
Bug ClassLogic Error
Tracker1996495
Fix commitdea366bd49ec (firefox) +536/-23
CISA KEVNot listed
Creditedtiebuchen
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
if
dom/base/Document.cpp
modified
Read
dom/security/PolicyContainer.cpp
modified
if
dom/security/PolicyContainer.cpp
modified

Files Changed

  • dom/base/Document.cpp
  • dom/security/PolicyContainer.cpp
  • dom/security/PolicyContainer.h
  • dom/serviceworkers/ServiceWorkerManager.cpp
  • dom/serviceworkers/ServiceWorkerPrivate.cpp
  • dom/serviceworkers/ServiceWorkerRegisterJob.cpp
  • dom/serviceworkers/ServiceWorkerRegisterJob.h
  • dom/serviceworkers/ServiceWorkerRegistrationInfo.h
  • dom/workers/ScriptLoader.cpp
  • dom/workers/ScriptLoader.h
  • dom/workers/WorkerLoadInfo.h
  • dom/workers/WorkerPrivate.cpp
  • dom/workers/WorkerPrivate.h
  • dom/workers/remoteworkers/RemoteWorkerChild.cpp
  • ipc/glue/PBackgroundSharedTypes.ipdlh
  • netwerk/base/LNAPermissionRequest.cpp
  • netwerk/base/LoadInfo.cpp
  • netwerk/protocol/http/nsHttpChannel.cpp
  • netwerk/test/browser/browser.toml
  • netwerk/test/browser/browser_test_lna_worker.js
  • netwerk/test/browser/fetch_from_tracker.sjs
  • netwerk/test/browser/service_worker_lna.sjs
  • netwerk/test/browser/shared_worker_lna.js
  • netwerk/test/browser/worker_lna.js
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index 040e07c63f8..e0a568f6c08 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -3761,6 +3761,15 @@ nsresult Document::InitPolicyContainer(nsIChannel* aChannel) {
     mPolicyContainer = new PolicyContainer();
   }
 
+  // Propagate the document's IP address space to the policy container so that
+  // workers inheriting this container can perform Local Network Access checks
+  // (workers don't have a browsing context to read this from).
+  nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+  nsILoadInfo::IPAddressSpace ipAddressSpace = loadInfo->GetIpAddressSpace();
+  if (ipAddressSpace != nsILoadInfo::Unknown) {
+    mPolicyContainer->SetIPAddressSpace(ipAddressSpace);
+  }
+
   return NS_OK;
 }
 
diff --git a/dom/security/PolicyContainer.cpp b/dom/security/PolicyContainer.cpp
index 42fb86b8d1d..59e62a4a144 100644
--- a/dom/security/PolicyContainer.cpp
+++ b/dom/security/PolicyContainer.cpp
@@ -18,15 +18,14 @@ using namespace mozilla::dom;
 PolicyContainer::PolicyContainer() = default;
 PolicyContainer::~PolicyContainer() = default;
 
-constexpr static uint32_t kPolicyContainerSerializationVersion = 1;
+constexpr static uint32_t kPolicyContainerSerializationVersion = 2;
 
 NS_IMETHODIMP
 PolicyContainer::Read(nsIObjectInputStream* aStream) {
-  // Currently, we don't care about the version, but we might in the future.
   uint32_t version = 0;
   MOZ_TRY(aStream->Read32(&version));
 
-  if (version != kPolicyContainerSerializationVersion) {
+  if (version < 1 || version > kPolicyContainerSerializationVersion) {
     return NS_ERROR_FAILURE;
   }
 
@@ -61,6 +60,13 @@ PolicyContainer::Read(nsIObjectInputStream* aStream) {
   MOZ_TRY(
       NS_ReadOptionalObject(aStream, true, getter_AddRefs(integrityPolicy)));
   mIntegrityPolicy = do_QueryInterface(integrityPolicy);
+
+  if (version >= 2) {
+    uint16_t ipAS = 0;
+    MOZ_TRY(aStream->Read16(&ipAS));
+    mIPAddressSpace = static_cast<nsILoadInfo::IPAddressSpace>(ipAS);
+  }
+
   return NS_OK;
 }
 
@@ -77,6 +83,8 @@ PolicyContainer::Write(nsIObjectOutputStream* aStream) {
   // TODO(Bug 2017654): (De)Serialize the WAICT state as part of the
   // Policy-Container
 
+  MOZ_TRY(aStream->Write16(static_cast<uint16_t>(mIPAddressSpace)));
+
   return NS_OK;
 }
 
@@ -98,6 +106,8 @@ void PolicyContainer::ToArgs(const PolicyContainer* aPolicy,
                             integrityPolicyArgs);
     aArgs.integrityPolicy() = Some(integrityPolicyArgs);
   }
+
+  aArgs.ipAddressSpace() = aPolicy->mIPAddressSpace;
 }
 
 void PolicyContainer::FromArgs(const mozilla::ipc::PolicyContainerArgs& aArgs,
@@ -118,6 +128,8 @@ void PolicyContainer::FromArgs(const mozilla::ipc::PolicyContainerArgs& aArgs,
     policy->SetIntegrityPolicy(integrityPolicy);
   }
 
+  policy->SetIPAddressSpace(aArgs.ipAddressSpace());
+
   policy.forget(aPolicy);
 }
 
@@ -139,6 +151,8 @@ void PolicyContainer::InitFromOther(PolicyContainer* aOther) {
         IntegrityPolicy::Cast(aOther->mIntegrityPolicy));
     mIntegrityPolicy = integrityPolicy;
   }
+
+  mIPAddressSpace = aOther->mIPAddressSpace;
 }
 
 NS_IMETHODIMP PolicyContainer::InitFromCSP(nsIContentSecurityPolicy* aCSP) {
@@ -169,6 +183,10 @@ bool PolicyContainer::Equals(const PolicyContainer* aContainer,
     return false;
   }
 
+  if (aContainer->mIPAddressSpace != aOtherContainer->mIPAddressSpace) {
+    return false;
+  }
+
   // TODO(Bug 2017654): Handle equality for WAICT.
 
   return true;
@@ -223,6 +241,16 @@ IntegrityPolicyWAICT* PolicyContainer::GetIntegrityPolicyWAICT(
   return PolicyContainer::Cast(aPolicyContainer)->GetIntegrityPolicyWAICT();
 }
 
+// == IP Address Space ==
+nsILoadInfo::IPAddressSpace PolicyContainer::GetIPAddressSpace() const {
+  return mIPAddressSpace;
+}
+
+void PolicyContainer::SetIPAddressSpace(
+    nsILoadInfo::IPAddressSpace aIPAddressSpace) {
+  mIPAddressSpace = aIPAddressSpace;
+}
+
 NS_IMETHODIMP PolicyContainer::GetCsp(nsIContentSecurityPolicy** aCsp) {
   nsCOMPtr<nsIContentSecurityPolicy> csp = mCSP;
   csp.forget(aCsp);
diff --git a/dom/security/PolicyContainer.h b/dom/security/PolicyContainer.h
index 6e40023ab9b..ff17edd44f7 100644
--- a/dom/security/PolicyContainer.h
+++ b/dom/security/PolicyContainer.h
@@ -8,6 +8,7 @@
 #include "nsCOMPtr.h"
 #include "nsIContentSecurityPolicy.h"
 #include "nsIIntegrityPolicy.h"
+#include "nsILoadInfo.h"
 #include "nsIPolicyContainer.h"
 
 namespace mozilla::ipc {
@@ -77,10 +78,18 @@ class PolicyContainer : public nsIPolicyContainer {
   static mozilla::dom::IntegrityPolicyWAICT* GetIntegrityPolicyWAICT(
       const nsIPolicyContainer* aPolicyContainer);
 
+  // == IP Address Space ==
+  // Stored per https://wicg.github.io/local-network-access/#integration-with-html
+  // to allow worker contexts (which have no browsing context) to perform
+  // Local Network Access checks against their parent document's address space.
+  nsILoadInfo::IPAddressSpace GetIPAddressSpace() const;
+  void SetIPAddressSpace(nsILoadInfo::IPAddressSpace aIPAddressSpace);
+
  private:
   nsCOMPtr<nsIContentSecurityPolicy> mCSP;
   nsCOMPtr<nsIIntegrityPolicy> mIntegrityPolicy;
   RefPtr<mozilla::dom::IntegrityPolicyWAICT> mIntegrityPolicyWAICT;
+  nsILoadInfo::IPAddressSpace mIPAddressSpace = nsILoadInfo::Unknown;
 
  protected:
   virtual ~PolicyContainer();
diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp
index 069635c7d35..2ee2a2caae5 100644
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -930,9 +930,17 @@ RefPtr<ServiceWorkerRegistrationPromise> ServiceWorkerManager::Register(
 
   auto lifetime = DetermineLifetimeForClient(aClientInfo);
 
+  uint16_t ipAddressSpace = 0;
+  auto policyContainerArgs = aClientInfo.GetPolicyContainerArgs();
+  if (policyContainerArgs.isSome()) {
+    ipAddressSpace =
+        static_cast<uint16_t>(policyContainerArgs->ipAddressSpace());
+  }
+
   RefPtr<ServiceWorkerRegisterJob> job = new ServiceWorkerRegisterJob(
       principal, aScopeURL, aType, aScriptURL,
-      static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache), lifetime);
+      static_cast<ServiceWorkerUpdateViaCache>(aUpdateViaCache), lifetime,
+      ipAddressSpace);
 
   job->AppendResultCallback(cb);
   queue->ScheduleJob(job);
diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp
index fc9fe3a0306..c2d4023ebff 100644
--- a/dom/serviceworkers/ServiceWorkerPrivate.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -28,6 +28,7 @@
 #include "mozilla/StaticPrefs_privacy.h"
 #include "mozilla/StoragePrincipalHelper.h"
 #include "mozilla/dom/Client.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/dom/ClientIPCTypes.h"
 #include "mozilla/dom/ClientManager.h"
 #include "mozilla/dom/DOMTypes.h"
@@ -736,6 +737,11 @@ nsresult ServiceWorkerPrivate::Initialize() {
   mClientInfo->SetURL(mInfo->ScriptSpec());
   mClientInfo->SetFrameType(FrameType::None);
 
+  // Set the IP address space from the registration for LNA checks.
+  mozilla::ipc::PolicyContainerArgs policyContainerArgs;
+  policyContainerArgs.ipAddressSpace() =
+      static_cast<nsILoadInfo::IPAddressSpace>(regInfo->GetIPAddressSpace());
+  mClientInfo->SetPolicyContainerArgs(policyContainerArgs);
   WorkerOptions workerOptions;
   workerOptions.mCredentials = RequestCredentials::Omit;
   workerOptions.mType = mInfo->Type();
diff --git a/dom/serviceworkers/ServiceWorkerRegisterJob.cpp b/dom/serviceworkers/ServiceWorkerRegisterJob.cpp
index 9f448097851..2d394129650 100644
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml
index 64dd976b236..82ba3cdddd9 100644
--- a/netwerk/test/browser/browser.toml
+++ b/netwerk/test/browser/browser.toml
@@ -251,6 +251,17 @@ support-files = [
   "fetch_from_tracker.sjs",
 ]
 
+["browser_test_lna_worker.js"]
+support-files = [
+  "head_local_network_access.js",
+  "page_with_non_trackers.html",
+  "page_with_non_trackers.html^headers^",
+  "fetch_from_tracker.sjs",
+  "worker_lna.js",
+  "shared_worker_lna.js",
+  "service_worker_lna.sjs",
+]
+
 ["browser_test_local_network_access_feature_policy.js"]
 support-files = [
   "head_local_network_access.js",
diff --git a/netwerk/test/browser/browser_test_lna_worker.js b/netwerk/test/browser/browser_test_lna_worker.js
new file mode 100644
index 00000000000..c206cf1bec0
--- /dev/null
+++ b/netwerk/test/browser/browser_test_lna_worker.js
@@ -0,0 +1,141 @@
+"use strict";
+
+// Tests that Local Network Access (LNA) checks are enforced for requests
+// made from dedicated and shared workers.
+
+Services.scriptloader.loadSubScript(
+  new URL("head_local_network_access.js", gTestPath).href,
+  this
+);
+
+add_setup(async function () {
+  await setupLnaPrefs();
+  await SpecialPowers.pushPrefEnv({
+    set: [["dom.serviceWorkers.testing.enabled", true]],
+  });
+  await setupLnaServer();
+});
+
+requestLongerTimeout(5);
+
+const sharedWorkerTestCases = [
+  {
+    type: "shared-worker-fetch",
+    allowStatus: Cr.NS_OK,
+    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
+  },
+  {
+    type: "shared-worker-xhr",
+    allowStatus: Cr.NS_OK,
+    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
+  },
+];
+
+// Shared worker: denied without persistent permission (no prompt shown)
+add_task(async function test_lna_shared_worker_denied_without_permission() {
+  Services.prefs.setCharPref(
+    "network.lna.address_space.public.override",
+    "127.0.0.1:4443"
+  );
+  for (const test of sharedWorkerTestCases) {
+    const rand = Math.random();
+    await runSingleTestCase(
+      test,
+      rand,
+      test.denyStatus,
+      `Shared worker ${test.type} denied without persistent permission`
+    );
+  }
+  Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+// Shared worker: allowed with persistent permission (no prompt shown)
+add_task(async function test_lna_shared_worker_allowed_with_permission() {
+  Services.prefs.setCharPref(
+    "network.lna.address_space.public.override",
+    "127.0.0.1:4443"
+  );
+
+  // Add persistent loopback-network permission for example.com
+  let principal =
+    Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+      "https://example.com"
+    );
+  Services.perms.addFromPrincipal(
+    principal,
+    "loopback-network",
+    Services.perms.ALLOW_ACTION,
+    Services.perms.EXPIRE_NEVER
+  );
+
+  for (const test of sharedWorkerTestCases) {
+    const rand = Math.random();
+    await runSingleTestCase(
+      test,
+      rand,
+      test.allowStatus,
+      `Shared worker ${test.type} allowed with persistent permission`
+    );
+  }
+
+  Services.perms.removeFromPrincipal(principal, "loopback-network");
+  Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+const serviceWorkerTestCases = [
+  {
+    type: "service-worker-fetch",
+    allowStatus: Cr.NS_OK,
+    denyStatus: Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
+  },
+];
+
+// Service worker: denied without persistent permission (no prompt shown)
+add_task(async function test_lna_service_worker_denied_without_permission() {
+  Services.prefs.setCharPref(
+    "network.lna.address_space.public.override",
+    "127.0.0.1:4443"
+  );
+  for (const test of serviceWorkerTestCases) {
+    const rand = Math.random();
+    await runSingleTestCase(
+      test,
+      rand,
+      test.denyStatus,
+      `Service worker ${test.type} denied without persistent permission`
+    );
+  }
+  Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
+
+// Service worker: allowed with persistent permission (no prompt shown)
+add_task(async function test_lna_service_worker_allowed_with_permission() {
+  Services.prefs.setCharPref(
+    "network.lna.address_space.public.override",
+    "127.0.0.1:4443"
+  );
+
+  let principal =
+    Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+      "https://example.com"
+    );
+  Services.perms.addFromPrincipal(
+    principal,
+    "loopback-network",
+    Services.perms.ALLOW_ACTION,
+    Services.perms.EXPIRE_NEVER
+  );
+
+  for (const test of serviceWorkerTestCases) {
+    const rand = Math.random();
+    await runSingleTestCase(
+      test,
+      rand,
+      test.allowStatus,
+      `Service worker ${test.type} allowed with persistent permission`
+    );
+  }
+
+  Services.perms.removeFromPrincipal(principal, "loopback-network");
+  Services.prefs.clearUserPref("network.lna.address_space.public.override");
+});
diff --git a/netwerk/test/browser/fetch_from_tracker.sjs b/netwerk/test/browser/fetch_from_tracker.sjs
index d7c052fe68e..017e48a4410 100644
--- a/netwerk/test/browser/fetch_from_tracker.sjs
+++ b/netwerk/test/browser/fetch_from_tracker.sjs
@@ -106,6 +106,57 @@ function handleRequest(request, response) {
     }
   `;
 
+  let workerFetchScript = `
+    results.workerFetch = "PENDING";
+    const w = new Worker("worker_lna.js");
+    w.postMessage({ type: "worker-fetch", rand: "${rand}" });
+    w.onmessage = (e) => results.workerFetch = e.data.status;
+    w.onerror = () => results.workerFetch = "FAIL";
+  `;
+
+  let workerXhrScript = `
+    results.workerXhr = "PENDING";
+    const w = new Worker("worker_lna.js");
+    w.postMessage({ type: "worker-xhr", rand: "${rand}" });
+    w.onmessage = (e) => results.workerXhr = e.data.status;
+    w.onerror = () => results.workerXhr = "FAIL";
+  `;
+
+  let sharedWorkerFetchScript = `
+    results.sharedWorkerFetch = "PENDING";
+    const sw = new SharedWorker("shared_worker_lna.js");
+    sw.port.start();
+    sw.port.postMessage({ type: "shared-worker-fetch", rand: "${rand}" });
+    sw.port.onmessage = (e) => results.sharedWorkerFetch = e.data.status;
+    sw.onerror = () => results.sharedWorkerFetch = "FAIL";
+  `;
+
+  let sharedWorkerXhrScript = `
+    results.sharedWorkerXhr = "PENDING";
+    const sw = new SharedWorker("shared_worker_lna.js");
+    sw.port.start();
+    sw.port.postMessage({ type: "shared-worker-xhr", rand: "${rand}" });
+    sw.port.onmessage = (e) => results.sharedWorkerXhr = e.data.status;
+    sw.onerror = () => results.sharedWorkerXhr = "FAIL";
+  `;
+
+  let serviceWorkerFetchScript = `
+    results.serviceWorkerFetch = "PENDING";
+    navigator.serviceWorker.register("service_worker_lna.sjs", { scope: "./" })
+      .then(reg => {
+        return navigator.serviceWorker.ready;
+      })
+      .then(reg => {
+        navigator.serviceWorker.onmessage = (e) => {
+          results.serviceWorkerFetch = e.data.status;
+        };
+        reg.active.postMessage({ type: "service-worker-fetch", rand: "${rand}" });
+      })
+      .catch((e) => {
+        results.serviceWorkerFetch = "FAIL";
+      });
+  `;
+
   switch (params.get("test")) {
     case "fetch":
       response.write(fetchScript);
@@ -137,6 +188,21 @@ function handleRequest(request, response) {
     case "websocket":
       response.write(websocketScript);
       return;
+    case "worker-fetch":
+      response.write(workerFetchScript);
+      return;
+    case "worker-xhr":
+      response.write(workerXhrScript);
+      return;
+    case "shared-worker-fetch":
+      response.write(sharedWorkerFetchScript);
+      return;
+    case "shared-worker-xhr":
+      response.write(sharedWorkerXhrScript);
+      return;
+    case "service-worker-fetch":
+      response.write(serviceWorkerFetchScript);
+      return;
   }
   response.write(`console.log("unknown test type")`);
 }
diff --git a/netwerk/test/browser/service_worker_lna.sjs b/netwerk/test/browser/service_worker_lna.sjs
new file mode 100644
index 00000000000..e4713f57b48
--- /dev/null
+++ b/netwerk/test/browser/service_worker_lna.sjs
@@ -0,0 +1,30 @@
+"use strict";
+
+function handleRequest(request, response) {
+  response.setHeader("Content-Type", "application/javascript", false);
+  response.write(`\
+self.addEventListener("install", function () {
+  self.skipWaiting();
+});
+self.addEventListener("activate", function (e) {
+  e.waitUntil(self.clients.claim());
+});
+self.addEventListener("message", function (e) {
+  var data = e.data;
+  var url = "http://localhost:21555/?type=" + data.type + "&rand=" + data.rand;
+  fetch(url).then(function () {
+    return self.clients.matchAll({ includeUncontrolled: true, type: "window" });
+  }).then(function (clients) {
+    for (var i = 0; i < clients.length; i++) {
+      clients[i].postMessage({ status: "OK" });
+    }
+  }).catch(function (ex) {
+    self.clients.matchAll({ includeUncontrolled: true, type: "window" }).then(function (clients) {
+      for (var i = 0; i < clients.length; i++) {
+        clients[i].postMessage({ status: "FAIL", error: ex.message });
+      }
+    });
+  });
+});
+`);
+}
diff --git a/netwerk/test/browser/shared_worker_lna.js b/netwerk/test/browser/shared_worker_lna.js
new file mode 100644
index 00000000000..cc61fc8c8bd
--- /dev/null
+++ b/netwerk/test/browser/shared_worker_lna.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
... (truncated)
Loading diff…