Firefox · DOM
CVE-2026-16370
Logic Error in DOM
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/base/Document.cpp |
modified | |
Readdom/security/PolicyContainer.cpp |
modified | |
ifdom/security/PolicyContainer.cpp |
modified |
Files Changed
dom/base/Document.cppdom/security/PolicyContainer.cppdom/security/PolicyContainer.hdom/serviceworkers/ServiceWorkerManager.cppdom/serviceworkers/ServiceWorkerPrivate.cppdom/serviceworkers/ServiceWorkerRegisterJob.cppdom/serviceworkers/ServiceWorkerRegisterJob.hdom/serviceworkers/ServiceWorkerRegistrationInfo.hdom/workers/ScriptLoader.cppdom/workers/ScriptLoader.hdom/workers/WorkerLoadInfo.hdom/workers/WorkerPrivate.cppdom/workers/WorkerPrivate.hdom/workers/remoteworkers/RemoteWorkerChild.cppipc/glue/PBackgroundSharedTypes.ipdlhnetwerk/base/LNAPermissionRequest.cppnetwerk/base/LoadInfo.cppnetwerk/protocol/http/nsHttpChannel.cppnetwerk/test/browser/browser.tomlnetwerk/test/browser/browser_test_lna_worker.jsnetwerk/test/browser/fetch_from_tracker.sjsnetwerk/test/browser/service_worker_lna.sjsnetwerk/test/browser/shared_worker_lna.jsnetwerk/test/browser/worker_lna.js
Patch
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…
References
On This Page