Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMitigation bypass in the Enterprise Policies component
ComponentCore
Bug ClassLogic Error
Tracker2073382
Fix commit5268ab79bb2f (firefox) +206/-5
CISA KEVNot listed
CreditedHyeonSeung Kim
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
if
browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
modified
asyncOnChannelRedirect
browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
modified
shouldLoad
browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
modified
observe
browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
modified
for
browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
modified
add_task
browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
modified
if
browser/components/enterprisepolicies/tests/browser/cached301.sjs
modified
if
browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
modified

Files Changed

  • browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
  • browser/components/enterprisepolicies/tests/browser/302exception.sjs
  • browser/components/enterprisepolicies/tests/browser/browser.toml
  • browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
  • browser/components/enterprisepolicies/tests/browser/cached301.sjs
  • browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
  • browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js
diff --git a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
index 7ece7f52632..3c96834ed51 100644
--- a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
+++ b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs
@@ -117,16 +117,43 @@ export let WebsiteFilter = {
         false,
         true
       );
+
+      // Backstop for redirects the observers below never see.
+      Services.catMan.addCategoryEntry(
+        "net-channel-event-sinks",
+        this.contractID,
+        this.contractID,
+        false,
+        true
+      );
     }
-    // We have to do this to catch 30X redirects.
+    // Cancelling here, rather than in the event sink, is what gives the user
+    // the error page.
     // See bug 456957.
     if (!this._observerAdded) {
       this._observerAdded = true;
-      // We rely on weak references, so we never remove this observer.
+      // We rely on weak references, so we never remove these observers.
+      // A 30X can also come from the cache, so all three topics matter.
       Services.obs.addObserver(this, "http-on-examine-response", true);
+      Services.obs.addObserver(this, "http-on-examine-cached-response", true);
+      Services.obs.addObserver(this, "http-on-examine-merged-response", true);
     }
   },
 
+  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+    let contentType = newChannel.loadInfo.externalContentPolicyType;
+    if (
+      (contentType == Ci.nsIContentPolicy.TYPE_DOCUMENT ||
+        contentType == Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) &&
+      !this.isAllowed(newChannel.URI.spec)
+    ) {
+      oldChannel.cancel(Cr.NS_ERROR_BLOCKED_BY_POLICY);
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_BLOCKED_BY_POLICY);
+      return;
+    }
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  },
+
   shouldLoad(contentLocation, loadInfo) {
     let contentType = loadInfo.externalContentPolicyType;
     let url = contentLocation.spec.toLowerCase();
@@ -151,10 +178,16 @@ export let WebsiteFilter = {
   observe(subject) {
     try {
       let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+      if (channel.responseStatus < 300 || channel.responseStatus >= 400) {
+        return;
+      }
+      // isDocument alone misses document loads whose channel has had
+      // LOAD_DOCUMENT_URI cleared.
+      let contentType = channel.loadInfo.externalContentPolicyType;
       if (
-        !channel.isDocument ||
-        channel.responseStatus < 300 ||
-        channel.responseStatus >= 400
+        !channel.isDocument &&
+        contentType != Ci.nsIContentPolicy.TYPE_DOCUMENT &&
+        contentType != Ci.nsIContentPolicy.TYPE_SUBDOCUMENT
       ) {
         return;
       }
@@ -174,6 +207,7 @@ export let WebsiteFilter = {
   classID: Components.ID("{c0bbb557-813e-4e25-809d-b46a531a258f}"),
   QueryInterface: ChromeUtils.generateQI([
     "nsIContentPolicy",
+    "nsIChannelEventSink",
     "nsIObserver",
     "nsISupportsWeakReference",
   ]),
diff --git a/browser/components/enterprisepolicies/tests/browser/302exception.sjs b/browser/components/enterprisepolicies/tests/browser/302exception.sjs
new file mode 100644
index 00000000000..aa8e255a34f
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/302exception.sjs
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+  response.setStatusLine(request.httpVersion, 302, "Found");
+  response.setHeader("Location", "policy_websitefilter_exception.html");
+}
diff --git a/browser/components/enterprisepolicies/tests/browser/browser.toml b/browser/components/enterprisepolicies/tests/browser/browser.toml
index d7047b3e827..5bf21068b24 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser.toml
+++ b/browser/components/enterprisepolicies/tests/browser/browser.toml
@@ -14,7 +14,11 @@ support-files = [
   "extensionsettings.html",
   "301.sjs",
   "302.sjs",
+  "302exception.sjs",
   "404.sjs",
+  "cached301.sjs",
+  "websitefilter_sw.html",
+  "websitefilter_sw.js",
 ]
 
 ["browser_policies_about_policies.js"]
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
index 0fa0dd4c809..4ad71fa9d92 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
@@ -8,6 +8,25 @@ const BLOCKED_PAGE = "policy_websitefilter_block.html";
 const EXCEPTION_PAGE = "policy_websitefilter_exception.html";
 const SAVELINKAS_PAGE = "policy_websitefilter_savelink.html";
 
+function unregisterAllServiceWorkers() {
+  let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+    Ci.nsIServiceWorkerManager
+  );
+  let regs = swm.getAllRegistrations();
+  let promises = [];
+  for (let i = 0; i < regs.length; i++) {
+    let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+    let { promise, resolve, reject } = Promise.withResolvers();
+    swm.unregister(
+      reg.principal,
+      { unregisterSucceeded: resolve, unregisterFailed: reject },
+      reg.scope
+    );
+    promises.push(promise);
+  }
+  return Promise.all(promises);
+}
+
 async function clearWebsiteFilter() {
   await setupPolicyEngineWithJson({
     policies: {
@@ -52,6 +71,24 @@ add_task(async function test_http() {
   await checkBlockedPage(SUPPORT_FILES_PATH + "301.sjs", true);
 
   await checkBlockedPage(SUPPORT_FILES_PATH + "302.sjs", true);
+
+  await checkBlockedPage("view-source:" + SUPPORT_FILES_PATH + "301.sjs", true);
+
+  await checkBlockedPage("view-source:" + SUPPORT_FILES_PATH + "302.sjs", true);
+
+  // A redirect to an allowed destination must still complete.
+  let tab = BrowserTestUtils.addTab(
+    gBrowser,
+    SUPPORT_FILES_PATH + "302exception.sjs"
+  );
+  await BrowserTestUtils.browserLoaded(
+    tab.linkedBrowser,
+    false,
+    SUPPORT_FILES_PATH + EXCEPTION_PAGE
+  );
+  ok(true, "Redirect to an allowed page was not blocked");
+  BrowserTestUtils.removeTab(tab);
+
   await clearWebsiteFilter();
 });
 
@@ -189,3 +226,74 @@ add_task(async function test_http_json_policy() {
   await checkBlockedPage(SUPPORT_FILES_PATH + "302.sjs", true);
   await clearWebsiteFilter();
 });
+
+async function redirectHits() {
+  let response = await fetch(SUPPORT_FILES_PATH + "cached301.sjs?hits");
+  return parseInt(await response.text(), 10);
+}
+
+add_task(async function test_cached_redirect() {
+  Services.cache2.clear();
+  let hits = await redirectHits();
+
+  // Prime the HTTP cache with the redirect while no filter is in effect.
+  let tab = BrowserTestUtils.addTab(
+    gBrowser,
+    SUPPORT_FILES_PATH + "cached301.sjs"
+  );
+  await BrowserTestUtils.browserLoaded(
+    tab.linkedBrowser,
+    false,
+    SUPPORT_FILES_PATH + BLOCKED_PAGE
+  );
+  BrowserTestUtils.removeTab(tab);
+  is(await redirectHits(), hits + 1, "Redirect was served from the network");
+
+  await setupPolicyEngineWithJson({
+    policies: {
+      WebsiteFilter: {
+        Block: ["*://mochi.test/*policy_websitefilter_*"],
+      },
+    },
+  });
+
+  await checkBlockedPage(SUPPORT_FILES_PATH + "cached301.sjs", true);
+  is(await redirectHits(), hits + 1, "Redirect was served from the cache");
+
+  await clearWebsiteFilter();
+  Services.cache2.clear();
+});
+
+add_task(async function test_service_worker_redirect() {
+  // Service workers need a secure context, so this one case runs over https.
+  const HTTPS_PATH =
+    "https://example.com/browser/browser/components/enterprisepolicies/tests/browser/";
+
+  await setupPolicyEngineWithJson({
+    policies: {
+      WebsiteFilter: {
+        Block: ["*://example.com/*policy_websitefilter_block*"],
+      },
+    },
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    HTTPS_PATH + "websitefilter_sw.html"
+  );
+  let status = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+    await ContentTaskUtils.waitForCondition(
+      () => content.document.getElementById("status").textContent != "pending",
+      "service worker registered"
+    );
+    return content.document.getElementById("status").textContent;
+  });
+  is(status, "ready", "Service worker is controlling the page");
+  BrowserTestUtils.removeTab(tab);
+
+  // The worker answers this navigation with a synthesized redirect.
+  await checkBlockedPage(HTTPS_PATH + "sw-redirect", true);
+
+  await unregisterAllServiceWorkers();
+  await clearWebsiteFilter();
+});
diff --git a/browser/components/enterprisepolicies/tests/browser/cached301.sjs b/browser/components/enterprisepolicies/tests/browser/cached301.sjs
new file mode 100644
index 00000000000..ac5d3ff0dd6
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/cached301.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+  let hits = parseInt(getState("hits") || "0", 10);
+
+  if (request.queryString == "hits") {
+    response.setHeader("Cache-Control", "no-store");
+    response.write("" + hits);
+    return;
+  }
+
+  setState("hits", "" + (hits + 1));
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Cache-Control", "max-age=3600");
+  response.setHeader("Location", "policy_websitefilter_block.html");
+}
diff --git a/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
new file mode 100644
index 00000000000..eb30c5d9aad
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html><head><meta charset="UTF-8"></head><body>
+<div id="status">pending</div>
+<script>
+(async () => {
+  try {
+    await navigator.serviceWorker.register("websitefilter_sw.js");
+    await navigator.serviceWorker.ready;
+    if (!navigator.serviceWorker.controller) {
+      await new Promise(r =>
+        navigator.serviceWorker.addEventListener("controllerchange", r, { once: true })
+      );
+    }
+    document.getElementById("status").textContent = "ready";
+  } catch (e) {
+    document.getElementById("status").textContent = "error:" + e;
+  }
+})();
+</script>
+</body></html>
diff --git a/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js
new file mode 100644
index 00000000000..6d0e76c577b
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js
@@ -0,0 +1,9 @@
+self.addEventListener("install", () => self.skipWaiting());
+self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
+self.addEventListener("fetch", event => {
+  if (event.request.url.includes("sw-redirect")) {
+    event.respondWith(
+      Response.redirect("policy_websitefilter_block.html", 302)
+    );
+  }
+});
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/components/enterprisepolicies/tests/browser/302exception.sjs b/browser/components/enterprisepolicies/tests/browser/302exception.sjs
new file mode 100644
index 00000000000..aa8e255a34f
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/302exception.sjs
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+  response.setStatusLine(request.httpVersion, 302, "Found");
+  response.setHeader("Location", "policy_websitefilter_exception.html");
+}
diff --git a/browser/components/enterprisepolicies/tests/browser/browser.toml b/browser/components/enterprisepolicies/tests/browser/browser.toml
index d7047b3e827..5bf21068b24 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser.toml
+++ b/browser/components/enterprisepolicies/tests/browser/browser.toml
@@ -14,7 +14,11 @@ support-files = [
   "extensionsettings.html",
   "301.sjs",
   "302.sjs",
+  "302exception.sjs",
   "404.sjs",
+  "cached301.sjs",
+  "websitefilter_sw.html",
+  "websitefilter_sw.js",
 ]
 
 ["browser_policies_about_policies.js"]
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
index 0fa0dd4c809..4ad71fa9d92 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_websitefilter.js
@@ -8,6 +8,25 @@ const BLOCKED_PAGE = "policy_websitefilter_block.html";
 const EXCEPTION_PAGE = "policy_websitefilter_exception.html";
 const SAVELINKAS_PAGE = "policy_websitefilter_savelink.html";
 
+function unregisterAllServiceWorkers() {
+  let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+    Ci.nsIServiceWorkerManager
+  );
+  let regs = swm.getAllRegistrations();
+  let promises = [];
+  for (let i = 0; i < regs.length; i++) {
+    let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+    let { promise, resolve, reject } = Promise.withResolvers();
+    swm.unregister(
+      reg.principal,
+      { unregisterSucceeded: resolve, unregisterFailed: reject },
+      reg.scope
+    );
+    promises.push(promise);
+  }
+  return Promise.all(promises);
+}
+
 async function clearWebsiteFilter() {
   await setupPolicyEngineWithJson({
     policies: {
@@ -52,6 +71,24 @@ add_task(async function test_http() {
   await checkBlockedPage(SUPPORT_FILES_PATH + "301.sjs", true);
 
   await checkBlockedPage(SUPPORT_FILES_PATH + "302.sjs", true);
+
+  await checkBlockedPage("view-source:" + SUPPORT_FILES_PATH + "301.sjs", true);
+
+  await checkBlockedPage("view-source:" + SUPPORT_FILES_PATH + "302.sjs", true);
+
+  // A redirect to an allowed destination must still complete.
+  let tab = BrowserTestUtils.addTab(
+    gBrowser,
+    SUPPORT_FILES_PATH + "302exception.sjs"
+  );
+  await BrowserTestUtils.browserLoaded(
+    tab.linkedBrowser,
+    false,
+    SUPPORT_FILES_PATH + EXCEPTION_PAGE
+  );
+  ok(true, "Redirect to an allowed page was not blocked");
+  BrowserTestUtils.removeTab(tab);
+
   await clearWebsiteFilter();
 });
 
@@ -189,3 +226,74 @@ add_task(async function test_http_json_policy() {
   await checkBlockedPage(SUPPORT_FILES_PATH + "302.sjs", true);
   await clearWebsiteFilter();
 });
+
+async function redirectHits() {
+  let response = await fetch(SUPPORT_FILES_PATH + "cached301.sjs?hits");
+  return parseInt(await response.text(), 10);
+}
+
+add_task(async function test_cached_redirect() {
+  Services.cache2.clear();
+  let hits = await redirectHits();
+
+  // Prime the HTTP cache with the redirect while no filter is in effect.
+  let tab = BrowserTestUtils.addTab(
+    gBrowser,
+    SUPPORT_FILES_PATH + "cached301.sjs"
+  );
+  await BrowserTestUtils.browserLoaded(
+    tab.linkedBrowser,
+    false,
+    SUPPORT_FILES_PATH + BLOCKED_PAGE
+  );
+  BrowserTestUtils.removeTab(tab);
+  is(await redirectHits(), hits + 1, "Redirect was served from the network");
+
+  await setupPolicyEngineWithJson({
+    policies: {
+      WebsiteFilter: {
+        Block: ["*://mochi.test/*policy_websitefilter_*"],
+      },
+    },
+  });
+
+  await checkBlockedPage(SUPPORT_FILES_PATH + "cached301.sjs", true);
+  is(await redirectHits(), hits + 1, "Redirect was served from the cache");
+
+  await clearWebsiteFilter();
+  Services.cache2.clear();
+});
+
+add_task(async function test_service_worker_redirect() {
+  // Service workers need a secure context, so this one case runs over https.
+  const HTTPS_PATH =
+    "https://example.com/browser/browser/components/enterprisepolicies/tests/browser/";
+
+  await setupPolicyEngineWithJson({
+    policies: {
+      WebsiteFilter: {
+        Block: ["*://example.com/*policy_websitefilter_block*"],
+      },
+    },
+  });
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    HTTPS_PATH + "websitefilter_sw.html"
+  );
+  let status = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+    await ContentTaskUtils.waitForCondition(
+      () => content.document.getElementById("status").textContent != "pending",
+      "service worker registered"
+    );
+    return content.document.getElementById("status").textContent;
+  });
+  is(status, "ready", "Service worker is controlling the page");
+  BrowserTestUtils.removeTab(tab);
+
+  // The worker answers this navigation with a synthesized redirect.
+  await checkBlockedPage(HTTPS_PATH + "sw-redirect", true);
+
+  await unregisterAllServiceWorkers();
+  await clearWebsiteFilter();
+});
diff --git a/browser/components/enterprisepolicies/tests/browser/cached301.sjs b/browser/components/enterprisepolicies/tests/browser/cached301.sjs
new file mode 100644
index 00000000000..ac5d3ff0dd6
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/cached301.sjs
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function handleRequest(request, response) {
+  let hits = parseInt(getState("hits") || "0", 10);
+
+  if (request.queryString == "hits") {
+    response.setHeader("Cache-Control", "no-store");
+    response.write("" + hits);
+    return;
+  }
+
+  setState("hits", "" + (hits + 1));
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Cache-Control", "max-age=3600");
+  response.setHeader("Location", "policy_websitefilter_block.html");
+}
diff --git a/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
new file mode 100644
index 00000000000..eb30c5d9aad
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html><head><meta charset="UTF-8"></head><body>
+<div id="status">pending</div>
+<script>
+(async () => {
+  try {
+    await navigator.serviceWorker.register("websitefilter_sw.js");
+    await navigator.serviceWorker.ready;
+    if (!navigator.serviceWorker.controller) {
+      await new Promise(r =>
+        navigator.serviceWorker.addEventListener("controllerchange", r, { once: true })
+      );
+    }
+    document.getElementById("status").textContent = "ready";
+  } catch (e) {
+    document.getElementById("status").textContent = "error:" + e;
+  }
+})();
+</script>
+</body></html>
diff --git a/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js
new file mode 100644
index 00000000000..6d0e76c577b
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/websitefilter_sw.js
@@ -0,0 +1,9 @@
+self.addEventListener("install", () => self.skipWaiting());
+self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
+self.addEventListener("fetch", event => {
+  if (event.request.url.includes("sw-redirect")) {
+    event.respondWith(
+      Response.redirect("policy_websitefilter_block.html", 302)
+    );
+  }
+});
Loading diff…