CVE-2026-74985
Overview
Background
- Enterprise Policies
- Firefox’s mechanism that lets administrators enforce configuration through a system
policies.jsonfile 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.jsonfrom 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.
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
- Set the trigger variable
A local user launches Firefox with the
XPCSHELL_TEST_PROFILE_DIRenvironment variable defined, makingisXpcshellevaluate true in a normal release build. - Redirect the policy source
With
isXpcshelltrue, the guard onbrowser.policies.alternatePathinJSONPoliciesProvideraccepts an attacker-controlledpolicies.jsonpath instead of the real system location. - Suppress real policies
On the Windows path,
WindowsGPOPoliciesProvider._readDataskipsROOT_KEY_LOCAL_MACHINE, so administrator-set machine policies are ignored. - Apply substituted policy The engine loads and enforces the attacker’s policy set, overriding or removing the enterprise-mandated configuration.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
onBeforeAddonsbrowser/components/enterprisepolicies/Policies.sys.mjs |
modified | |
ifbrowser/components/enterprisepolicies/Policies.sys.mjs |
modified | |
iftoolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs |
modified | |
_readDatatoolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs |
modified |
Files Changed
browser/components/enterprisepolicies/Policies.sys.mjstoolkit/components/backgroundtasks/tests/BackgroundTask_policies.sys.mjstoolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
Audit Directions
- Environment-variable trustFlag 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 codeReview every
Cu.isInAutomationalternative or supplement to ensure test-only bypasses cannot be reached in release builds. - Policy source overridesAudit all consumers of
browser.policies.alternatePathandPREF_ALTERNATE_GPOto confirm they are reachable only under genuinely trusted automation signals.
Patch
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);
Regression Test / PoC
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);