From 15fd9b371e30ed352cf2de2d241a0d0f7fafa9d1 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Fri, 8 May 2026 20:26:17 +0200 Subject: [PATCH] feat: allow to disable external JS extensions (#31826) --- lib/controller.ts | 42 +++++++++++++--------- lib/types/api.ts | 2 ++ lib/util/settings.schema.json | 7 ++++ lib/util/settings.ts | 2 ++ test/extensions/bridge.test.ts | 1 + test/extensions/externalConverters.test.ts | 11 ++++++ test/extensions/externalExtensions.test.ts | 11 ++++++ test/onboarding.test.ts | 3 ++ 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index f5e1d627b..1bd694c77 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -50,7 +50,6 @@ export class Controller { this.state = new State(this.eventBus, this.zigbee); this.restartCallback = restartCallback; this.exitCallback = exitCallback; - // Initialize extensions. this.extensionArgs = [ this.zigbee, @@ -62,22 +61,29 @@ export class Controller { this.restartCallback, this.addExtension, ]; + this.extensions = new Set(); - this.extensions = new Set([ - new ExtensionExternalConverters(...this.extensionArgs), - new ExtensionOnEvent(...this.extensionArgs), - new ExtensionBridge(...this.extensionArgs), - new ExtensionPublish(...this.extensionArgs), - new ExtensionReceive(...this.extensionArgs), - new ExtensionConfigure(...this.extensionArgs), - new ExtensionNetworkMap(...this.extensionArgs), - new ExtensionGroups(...this.extensionArgs), - new ExtensionBind(...this.extensionArgs), - new ExtensionOTAUpdate(...this.extensionArgs), - new ExtensionExternalExtensions(...this.extensionArgs), - new ExtensionAvailability(...this.extensionArgs), - new ExtensionHealth(...this.extensionArgs), - ]); + if (settings.get().advanced.enable_external_js) { + this.extensions.add(new ExtensionExternalConverters(...this.extensionArgs)); + } else { + logger.info("External JS (converters/extensions) is disabled"); + } + + this.extensions.add(new ExtensionOnEvent(...this.extensionArgs)); + this.extensions.add(new ExtensionBridge(...this.extensionArgs)); + this.extensions.add(new ExtensionPublish(...this.extensionArgs)); + this.extensions.add(new ExtensionReceive(...this.extensionArgs)); + this.extensions.add(new ExtensionConfigure(...this.extensionArgs)); + this.extensions.add(new ExtensionNetworkMap(...this.extensionArgs)); + this.extensions.add(new ExtensionGroups(...this.extensionArgs)); + this.extensions.add(new ExtensionBind(...this.extensionArgs)); + this.extensions.add(new ExtensionOTAUpdate(...this.extensionArgs)); + this.extensions.add(new ExtensionAvailability(...this.extensionArgs)); + this.extensions.add(new ExtensionHealth(...this.extensionArgs)); + + if (settings.get().advanced.enable_external_js) { + this.extensions.add(new ExtensionExternalExtensions(...this.extensionArgs)); + } } async start(): Promise { @@ -247,10 +253,12 @@ export class Controller { } const existingExtension = this.getExtension(name); + if (existingExtension) { await this.removeExtension(existingExtension); } - await this.extensions.add(extension); + + this.extensions.add(extension); } else { switch (name) { case "Frontend": { diff --git a/lib/types/api.ts b/lib/types/api.ts index 4e45e070b..a20eab946 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -215,6 +215,8 @@ export interface Zigbee2MQTTSettings { timestamp_format: string; output: "json" | "attribute" | "attribute_and_json"; transmit_power?: number; + /** 3.0: default to false (JSON schema & settings `defaults`) */ + enable_external_js: boolean; }; health: { /** in minutes */ diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index 1e5134132..6f8e743e0 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -808,6 +808,13 @@ "title": "MQTT output type", "description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)", "default": "json" + }, + "enable_external_js": { + "type": "boolean", + "title": "Enable external JS", + "description": "Enable external JavaScript (extensions and converters) that can execute arbitrary user-provided code. WARNING: If unused, it is advised to disable this.", + "default": true, + "requiresRestart": true } } }, diff --git a/lib/util/settings.ts b/lib/util/settings.ts index a41d56300..38dc2861a 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -113,6 +113,7 @@ export const defaults = { network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], timestamp_format: "YYYY-MM-DD HH:mm:ss", output: "json", + enable_external_js: true, }, health: { interval: 10, @@ -167,6 +168,7 @@ export function writeMinimalDefaults(): void { network_key: "GENERATE", pan_id: "GENERATE", ext_pan_id: "GENERATE", + enable_external_js: false, }, frontend: { enabled: defaults.frontend.enabled, diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index c9686c510..5a3a45f55 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -144,6 +144,7 @@ describe("Extension: Bridge", () => { output: "json", pan_id: 6754, timestamp_format: "YYYY-MM-DD HH:mm:ss", + enable_external_js: true, }, blocklist: [], device_options: {}, diff --git a/test/extensions/externalConverters.test.ts b/test/extensions/externalConverters.test.ts index fa5a71650..7e7333494 100644 --- a/test/extensions/externalConverters.test.ts +++ b/test/extensions/externalConverters.test.ts @@ -609,4 +609,15 @@ describe("Extension: ExternalConverters", () => { ); }); }); + + it("doesn't add extension when external JS disabled", async () => { + settings.set(["advanced", "enable_external_js"], false); + + controller = new Controller(vi.fn(), vi.fn()); + + await controller.start(); + await flushPromises(); + + expect(controller.getExtension("ExternalConverters")).toBeUndefined(); + }); }); diff --git a/test/extensions/externalExtensions.test.ts b/test/extensions/externalExtensions.test.ts index 480edf13a..db72dc996 100644 --- a/test/extensions/externalExtensions.test.ts +++ b/test/extensions/externalExtensions.test.ts @@ -365,4 +365,15 @@ describe("Extension: ExternalExtensions", () => { ); }); }); + + it("doesn't add extension when external JS disabled", async () => { + settings.set(["advanced", "enable_external_js"], false); + + controller = new Controller(vi.fn(), vi.fn()); + + await controller.start(); + await flushPromises(); + + expect(controller.getExtension("ExternalExtensions")).toBeUndefined(); + }); }); diff --git a/test/onboarding.test.ts b/test/onboarding.test.ts index 08ba99855..513fc4c0e 100644 --- a/test/onboarding.test.ts +++ b/test/onboarding.test.ts @@ -90,6 +90,7 @@ const SETTINGS_MINIMAL_DEFAULTS = { network_key: "GENERATE", pan_id: "GENERATE", ext_pan_id: "GENERATE", + enable_external_js: false, }, frontend: { enabled: settings.defaults.frontend!.enabled, @@ -147,6 +148,7 @@ const SAMPLE_SETTINGS_SAVE = { network_key: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], pan_id: 12345, ext_pan_id: [8, 7, 6, 5, 4, 3, 2, 1], + enable_external_js: false, }, frontend: { enabled: true, @@ -963,6 +965,7 @@ describe("Onboarding", () => { network_key: "GENERATE", pan_id: "GENERATE", ext_pan_id: "GENERATE", + enable_external_js: false, }, serial: { port: SAMPLE_SETTINGS_SAVE.serial.port,