diff --git a/lib/mqtt.ts b/lib/mqtt.ts index c2f7c3f42..542297519 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -176,6 +176,12 @@ export default class MQTT { skipLog = false, skipReceive = true, ): Promise { + if (topic.includes('+') || topic.includes('#')) { + // https://github.com/Koenkk/zigbee2mqtt/issues/26939#issuecomment-2772309646 + logger.error(`Topic '${topic}' includes wildcard characters, skipping publish.`); + return; + } + const defaultOptions = {qos: 0 as const, retain: false}; topic = `${base}/${topic}`; diff --git a/test/controller.test.ts b/test/controller.test.ts index 1267fe370..a71e8a1df 100644 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -283,6 +283,20 @@ describe('Controller', () => { controller.mqtt.client.reconnecting = false; }); + it('Should not allow publishing wildcard characters in topic', async () => { + await controller.start(); + await flushPromises(); + mockMQTTPublishAsync.mockClear(); + // @ts-expect-error private + await controller.mqtt.publish('z2m/#/status', 'empty'); + expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0); + expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/#/status' includes wildcard characters, skipping publish.`); + // @ts-expect-error private + await controller.mqtt.publish('z2m/+/status', 'empty'); + expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0); + expect(mockLogger.error).toHaveBeenCalledWith(`Topic 'z2m/+/status' includes wildcard characters, skipping publish.`); + }); + it('Load empty state when state file does not exist', async () => { data.removeState(); await controller.start();