Firefox · Toolkit
CVE-2026-92073
Logic Error in Toolkit
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbrowser/components/preferences/extensionControlled.js |
modified | |
iftoolkit/content/preferences/Setting.mjs |
modified | |
_clearControllingExtensionInfotoolkit/content/preferences/Setting.mjs |
modified | |
add_taskbrowser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html |
modified |
Files Changed
browser/components/preferences/extensionControlled.jsbrowser/components/preferences/tests/chrome/test_setting_control_extension_controlled.htmlbrowser/components/preferences/widgets/setting-control/setting-control.mjsbrowser/components/preferences/widgets/setting-control/setting-control.stories.mjstoolkit/content/preferences/Setting.mjs
Patch
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 = "ext-locked@mochi.test";
+ 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 = "ext-locked@mochi.test";
+ 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…
References
On This Page