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
Tracker2062527
Fix commitb3a420c96614 (firefox) +80/-7
CISA KEVNot listed
CreditedTyler Maclachlan
Disclosed2026-09-15

Changed Functions

FunctionChangeNotes
if
browser/components/preferences/extensionControlled.js
modified
if
toolkit/content/preferences/Setting.mjs
modified
_clearControllingExtensionInfo
toolkit/content/preferences/Setting.mjs
modified
add_task
browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
modified

Files Changed

  • browser/components/preferences/extensionControlled.js
  • browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
  • browser/components/preferences/widgets/setting-control/setting-control.mjs
  • browser/components/preferences/widgets/setting-control/setting-control.stories.mjs
  • toolkit/content/preferences/Setting.mjs
diff --git a/browser/components/preferences/extensionControlled.js b/browser/components/preferences/extensionControlled.js
index 19a41ec7284..235a625edf9 100644
--- a/browser/components/preferences/extensionControlled.js
+++ b/browser/components/preferences/extensionControlled.js
@@ -194,7 +194,9 @@ async function showControllingExtension(settingName, addon) {
   setControllingExtensionDescription(description, addon, settingName);
 
   if (elements.button) {
-    elements.button.hidden = false;
+    elements.button.hidden = !(
+      addon.permissions & AddonManager.PERM_CAN_DISABLE
+    );
   }
 
   // Show the controlling extension row and hide the old label.
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.mjs b/browser/components/preferences/widgets/setting-control/setting-control.mjs
index 673f0af9f0f..1f0830a0459 100644
--- a/browser/components/preferences/widgets/setting-control/setting-control.mjs
+++ b/browser/components/preferences/widgets/setting-control/setting-control.mjs
@@ -508,12 +508,14 @@ export class SettingControl extends SettingElement {
               support-page=${supportPage}
             ></a>`
           : ""}
-        <moz-button
-          slot="actions"
-          @click=${this.disableExtension}
-          ?disabled=${this.isDisablingExtension}
-          data-l10n-id="disable-extension"
-        ></moz-button>
+        ${this.setting.controllingExtensionInfo.mayDisable
+          ? html`<moz-button
+              slot="actions"
+              @click=${this.disableExtension}
+              ?disabled=${this.isDisablingExtension}
+              data-l10n-id="disable-extension"
+            ></moz-button>`
+          : ""}
       </moz-message-bar>`;
     } else if (this.showEnableExtensionMessage) {
       messageBar = html`<moz-message-bar
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs b/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs
index 7b14dc0eb8d..d5bd917d0fd 100644
--- a/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs
+++ b/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs
@@ -143,6 +143,7 @@ ExtensionControlled.args = {
       l10nId: "extension-controlled-message",
       name: "My Extension",
       supportPage: "preferences",
+      mayDisable: true,
       // NOTE: allowControl defaults to false, but it can be set to true
       allowControl: false,
     },
diff --git a/toolkit/content/preferences/Setting.mjs b/toolkit/content/preferences/Setting.mjs
index 36ec5eb3d37..9fbde97de57 100644
--- a/toolkit/content/preferences/Setting.mjs
+++ b/toolkit/content/preferences/Setting.mjs
@@ -130,6 +130,7 @@ import { Preferences } from "chrome://global/content/preferences/Preferences.mjs
  * @property {string} [id] The controlling extension's id.
  * @property {string} [supportPage] A support page to show in the message.
  * @property {boolean} [allowControl] If the control should be enabled while controlled.
+ * @property {boolean} [mayDisable] If the controlling extension may be disabled.
  */
 
 /**
@@ -426,6 +427,9 @@ export class Setting extends EventEmitter {
       if (addon) {
         this.controllingExtensionInfo.name = addon.name;
         this.controllingExtensionInfo.id = info.id;
+        this.controllingExtensionInfo.mayDisable = !!(
+          addon.permissions & lazy.AddonManager.PERM_CAN_DISABLE
+        );
         this.emit("change");
         return;
       }
@@ -436,6 +440,7 @@ export class Setting extends EventEmitter {
   _clearControllingExtensionInfo() {
     delete this.controllingExtensionInfo.id;
     delete this.controllingExtensionInfo.name;
+    delete this.controllingExtensionInfo.mayDisable;
     delete this.controllingExtensionInfo.supportPage;
     // Request an update to the setting control so the UI is in the correct state
     this.onChange();
diff --git a/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html b/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
index 862b22589b1..3a868b382e6 100644
--- a/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
+++ b/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
@@ -40,6 +40,9 @@
       const { Assert } = ChromeUtils.importESModule(
         "resource://testing-common/Assert.sys.mjs"
       );
+      const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+        "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+      );
       // Import the ExtensionSettingsStore and AddonManager as we need
       // them to correctly disable the extension on click and re-enable
       // the extension in the same task.
@@ -844,6 +847,66 @@
         await extension.unload();
         control.remove();
       });
+
+      add_task(async function testPolicyLockedExtensionControlledSetting() {
+        const SETTING_ID = "policy-locked-setting";
+        const ADDON_ID = "[email protected]";
+        const ADDON_NAME = "Ext Locked";
+        const STORE_ID = "privacy.containers";
+        const TEST_FLUENT_ID = "test-fluent-id";
+        await SpecialPowers.pushPrefEnv({
+          set: [[PREF_ID, false]],
+        });
+        let extension = ExtensionTestUtils.loadExtension({
+          manifest: {
+            browser_specific_settings: { gecko: { id: ADDON_ID } },
+            name: ADDON_NAME,
+            permissions: ["contextualIdentities", "cookies"],
+          },
+          useAddonManager: "temporary",
+        });
+        await extension.startup();
+
+        // Lock the extension before rendering, so that the control picks up
+        // the policy when it looks up the controlling extension.
+        await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+          policies: { Extensions: { Locked: [ADDON_ID] } },
+        });
+
+        Preferences.addSetting({
+          id: SETTING_ID,
+          pref: PREF_ID,
+          controllingExtensionInfo: {
+            storeId: STORE_ID,
+            l10nId: TEST_FLUENT_ID,
+          },
+        });
+
+        let control = await renderTemplate({
+          l10nId: TEST_FLUENT_ID,
+          id: SETTING_ID,
+        });
+
+        await TestUtils.waitForCondition(
+          () => control.controlEl.disabled,
+          "Wait for the control to become disabled as a side-effect of controlling extension metadata to be found"
+        );
+
+        let messageBar = control.querySelector("moz-message-bar");
+        ok(
+          messageBar,
+          "There should still be an extension controlled message bar element"
+        );
+        is(
+          messageBar.querySelector("moz-button[slot='actions']"),
+          null,
+          "There should be no disable button when policy forbids disabling"
+        );
+
+        await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+        await extension.unload();
+        control.remove();
+      });
     </script>
   </head>
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html b/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
index 862b22589b1..3a868b382e6 100644
--- a/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
+++ b/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
@@ -40,6 +40,9 @@
       const { Assert } = ChromeUtils.importESModule(
         "resource://testing-common/Assert.sys.mjs"
       );
+      const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+        "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+      );
       // Import the ExtensionSettingsStore and AddonManager as we need
       // them to correctly disable the extension on click and re-enable
       // the extension in the same task.
@@ -844,6 +847,66 @@
         await extension.unload();
         control.remove();
       });
+
+      add_task(async function testPolicyLockedExtensionControlledSetting() {
+        const SETTING_ID = "policy-locked-setting";
+        const ADDON_ID = "[email protected]";
+        const ADDON_NAME = "Ext Locked";
+        const STORE_ID = "privacy.containers";
+        const TEST_FLUENT_ID = "test-fluent-id";
+        await SpecialPowers.pushPrefEnv({
+          set: [[PREF_ID, false]],
+        });
+        let extension = ExtensionTestUtils.loadExtension({
+          manifest: {
+            browser_specific_settings: { gecko: { id: ADDON_ID } },
+            name: ADDON_NAME,
+            permissions: ["contextualIdentities", "cookies"],
+          },
+          useAddonManager: "temporary",
+        });
+        await extension.startup();
+
+        // Lock the extension before rendering, so that the control picks up
+        // the policy when it looks up the controlling extension.
+        await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+          policies: { Extensions: { Locked: [ADDON_ID] } },
+        });
+
+        Preferences.addSetting({
+          id: SETTING_ID,
+          pref: PREF_ID,
+          controllingExtensionInfo: {
+            storeId: STORE_ID,
+            l10nId: TEST_FLUENT_ID,
+          },
+        });
+
+        let control = await renderTemplate({
+          l10nId: TEST_FLUENT_ID,
+          id: SETTING_ID,
+        });
+
+        await TestUtils.waitForCondition(
+          () => control.controlEl.disabled,
+          "Wait for the control to become disabled as a side-effect of controlling extension metadata to be found"
+        );
+
+        let messageBar = control.querySelector("moz-message-bar");
+        ok(
+          messageBar,
+          "There should still be an extension controlled message bar element"
+        );
+        is(
+          messageBar.querySelector("moz-button[slot='actions']"),
+          null,
+          "There should be no disable button when policy forbids disabling"
+        );
+
+        await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+        await extension.unload();
+        control.remove();
+      });
     </script>
   </head>
Loading diff…