Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionPrivilege escalation in the Enterprise Policies component
ComponentToolkit
Bug ClassLogic Error
Tracker2059825
Fix commita4f93c656861 (firefox) +16/-12
CISA KEVNot listed
CreditedThe Mozilla Fuzzing Team
Disclosed2026-08-18

Background

Enterprise Policies
Firefox’s mechanism that lets administrators enforce configuration through a system policies.json file or platform GPO/registry entries.
`Cu.isInAutomation`
A trusted runtime flag that is true only when the browser is deliberately started in an automation/test harness and cannot be toggled in release builds.
`XPCSHELL_TEST_PROFILE_DIR`
An operating-system environment variable set by the xpcshell test harness to point at the test profile directory.
`browser.policies.alternatePath`
A preference that redirects the policy engine to read its policies.json from an alternate location instead of the real system path.

Root Cause Analysis

The policy engine gated several testing-only escape hatches on isXpcshell, a flag derived from Services.env.exists("XPCSHELL_TEST_PROFILE_DIR"), an ordinary environment variable rather than a build- or automation-controlled signal. Because environment variables are attacker-influenceable in the process’s launch context, any release build launched with XPCSHELL_TEST_PROFILE_DIR set would satisfy isXpcshell, allowing code paths meant only for tests — reading policies from browser.policies.alternatePath, skipping machine-level GPO/registry policies, and clearing blocked pages — to run outside automation. This violated the invariant that enterprise-policy enforcement can never be bypassed or redirected in a shipping build, effectively letting a local user substitute their own policy source.

The fix deletes the isXpcshell constant in Policies.sys.mjs and EnterprisePoliciesParent.sys.mjs and keys every testing escape solely on Cu.isInAutomation, which release builds cannot assert. The background-task test that previously relied on the env-var path is updated to explicitly opt into automation via the security.turn_off_all_security_so_that_viruses_can_take_over_this_computer pref so legitimate tests still function.

Key insight
The single mistake was using an attacker-settable environment variable (XPCSHELL_TEST_PROFILE_DIR) as a proxy for “we are in a test,” so the fix removes that check entirely and relies only on the tamper-resistant Cu.isInAutomation flag.

Attack Path

  1. Set the trigger variable A local user launches Firefox with the XPCSHELL_TEST_PROFILE_DIR environment variable defined, making isXpcshell evaluate true in a normal release build.
  2. Redirect the policy source With isXpcshell true, the guard on browser.policies.alternatePath in JSONPoliciesProvider accepts an attacker-controlled policies.json path instead of the real system location.
  3. Suppress real policies On the Windows path, WindowsGPOPoliciesProvider._readData skips ROOT_KEY_LOCAL_MACHINE, so administrator-set machine policies are ignored.
  4. Apply substituted policy The engine loads and enforces the attacker’s policy set, overriding or removing the enterprise-mandated configuration.

Impact Assessment

An attacker with local process-launch control gains the ability to bypass or replace enforced enterprise policies, defeating administrator-imposed restrictions in the parent (privileged chrome) process. The precondition is the ability to set an environment variable on the Firefox launch, so this is a local privilege/restriction-escalation rather than a remote vector, consistent with the low-severity rating.

Changed Functions

FunctionChangeNotes
onBeforeAddons
browser/components/enterprisepolicies/Policies.sys.mjs
modified
if
browser/components/enterprisepolicies/Policies.sys.mjs
modified
if
toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
modified
_readData
toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
modified

Files Changed

  • browser/components/enterprisepolicies/Policies.sys.mjs
  • toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
  • toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs

Audit Directions

  • Environment-variable trust
    Flag any security or test gate that keys on Services.env.exists(...) or env-var values, since these are attacker-influenceable in the launch context and must not gate privileged behavior.
  • Test escape hatches in shipping code
    Review every Cu.isInAutomation alternative or supplement to ensure test-only bypasses cannot be reached in release builds.
  • Policy source overrides
    Audit all consumers of browser.policies.alternatePath and PREF_ALTERNATE_GPO to confirm they are reachable only under genuinely trusted automation signals.
diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs
index 9aad627a7f6..2009e0af4e7 100644
--- a/browser/components/enterprisepolicies/Policies.sys.mjs
+++ b/browser/components/enterprisepolicies/Policies.sys.mjs
@@ -49,8 +49,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
 const PREF_LOGLEVEL = "browser.policies.loglevel";
 const BROWSER_DOCUMENT_URL = AppConstants.BROWSER_CHROME_URL;
 
-const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
-
 ChromeUtils.defineLazyGetter(lazy, "log", () => {
   let { ConsoleAPI } = ChromeUtils.importESModule(
     "resource://gre/modules/Console.sys.mjs"
@@ -95,7 +93,7 @@ export var Policies = {
   // Use the same timing that you used for setting up the policy.
   _cleanup: {
     onBeforeAddons() {
-      if (Cu.isInAutomation || isXpcshell) {
+      if (Cu.isInAutomation) {
         lazy.clearBlockedAboutPages();
       }
       Services.obs.notifyObservers(
diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
index 52971e49420..76ef9303765 100644
--- a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
+++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
@@ -55,14 +55,12 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => {
   });
 });
 
-const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
+// Testing escapes in this file must key on Cu.isInAutomation.
 
-// On Nightly under a test harness, ignore real system/user policies so a
-// developer's local policies.json or registry entries don't leak into tests.
-// Restricted to Nightly so release builds never expose a way to bypass
-// enterprise policies via a test env var.
+// On Nightly in automation, ignore real system/user policies so a developer's
+// local policies.json or registry entries don't leak into tests.
 function shouldIgnoreLocalPolicies() {
-  return AppConstants.NIGHTLY_BUILD && (Cu.isInAutomation || isXpcshell);
+  return AppConstants.NIGHTLY_BUILD && Cu.isInAutomation;
 }
 
 // We're only testing for empty objects, not
@@ -701,7 +699,7 @@ class JSONPoliciesProvider extends PoliciesProvider {
     // work as expected.
     if (
       alternatePath &&
-      (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) &&
+      (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD) &&
       (!configFile || !configFile.exists())
     ) {
       if (alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
@@ -773,7 +771,7 @@ class WindowsGPOPoliciesProvider extends PoliciesProvider {
     // user policies first and then replace them if necessary.
     this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
     // We don't access machine policies in testing
-    if (!Cu.isInAutomation && !isXpcshell) {
+    if (!Cu.isInAutomation) {
       this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
     }
   }
@@ -781,7 +779,7 @@ class WindowsGPOPoliciesProvider extends PoliciesProvider {
   _readData(wrk, root) {
     try {
       let regLocation = "SOFTWARE\\Policies";
-      if (Cu.isInAutomation || isXpcshell) {
+      if (Cu.isInAutomation) {
         let altLocation = Services.prefs.getStringPref(PREF_ALTERNATE_GPO, "");
         if (altLocation) {
           regLocation = altLocation;
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
index 4da86126cc0..2e213da105b 100644
--- a/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
@@ -5,6 +5,14 @@
 import { EnterprisePolicyTesting } from "resource://testing-common/EnterprisePolicyTesting.sys.mjs";
 
 export async function runBackgroundTask(commandLine) {
+  // This process runs with its own profile, so it doesn't inherit the xpcshell
+  // harness's automation prefs, which the policy engine requires to read
+  // `browser.policies.alternatePath`.
+  Services.prefs.setBoolPref(
+    "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+    true
+  );
+
   let filePath = commandLine.getArgument(0);
   await EnterprisePolicyTesting.setupPolicyEngineWithJson(filePath);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
index 4da86126cc0..2e213da105b 100644
--- a/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
+++ b/toolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjs
@@ -5,6 +5,14 @@
 import { EnterprisePolicyTesting } from "resource://testing-common/EnterprisePolicyTesting.sys.mjs";
 
 export async function runBackgroundTask(commandLine) {
+  // This process runs with its own profile, so it doesn't inherit the xpcshell
+  // harness's automation prefs, which the policy engine requires to read
+  // `browser.policies.alternatePath`.
+  Services.prefs.setBoolPref(
+    "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+    true
+  );
+
   let filePath = commandLine.getArgument(0);
   await EnterprisePolicyTesting.setupPolicyEngineWithJson(filePath);
Loading diff…