From c23ad91a145dbf2f2ff9e68a4e7b31a706f80965 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 07:09:34 +0100 Subject: [PATCH] fix(matrix): keep DM allowlist out of room commands --- .../src/matrix/monitor/access-state.test.ts | 20 +++++++++- .../matrix/src/matrix/monitor/access-state.ts | 2 +- .../matrix/src/matrix/monitor/handler.test.ts | 39 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/extensions/matrix/src/matrix/monitor/access-state.test.ts b/extensions/matrix/src/matrix/monitor/access-state.test.ts index b51e45d2e9b..f938e97a1b8 100644 --- a/extensions/matrix/src/matrix/monitor/access-state.test.ts +++ b/extensions/matrix/src/matrix/monitor/access-state.test.ts @@ -22,7 +22,7 @@ describe("resolveMatrixMonitorAccessState", () => { expect(state.roomUserMatch?.allowed).toBe(true); expect(state.groupAllowMatch?.allowed).toBe(false); expect(state.commandAuthorizers).toEqual([ - { configured: true, allowed: false }, + { configured: false, allowed: false }, { configured: true, allowed: true }, { configured: true, allowed: false }, ]); @@ -47,6 +47,24 @@ describe("resolveMatrixMonitorAccessState", () => { ]); }); + it("does not let configured DM allowFrom authorize room control commands", () => { + const state = resolveMatrixMonitorAccessState({ + allowFrom: ["@owner:example.org"], + storeAllowFrom: [], + groupAllowFrom: ["@admin:example.org"], + roomUsers: [], + senderId: "@owner:example.org", + isRoom: true, + }); + + expect(state.directAllowMatch.allowed).toBe(true); + expect(state.commandAuthorizers).toEqual([ + { configured: false, allowed: false }, + { configured: false, allowed: false }, + { configured: true, allowed: false }, + ]); + }); + it("keeps room-user matching disabled for dm traffic", () => { const state = resolveMatrixMonitorAccessState({ allowFrom: [], diff --git a/extensions/matrix/src/matrix/monitor/access-state.ts b/extensions/matrix/src/matrix/monitor/access-state.ts index 586319420a9..0733778de96 100644 --- a/extensions/matrix/src/matrix/monitor/access-state.ts +++ b/extensions/matrix/src/matrix/monitor/access-state.ts @@ -37,7 +37,7 @@ export function resolveMatrixMonitorAccessState(params: { ]); const effectiveGroupAllowFrom = normalizeMatrixAllowList(params.groupAllowFrom); const effectiveRoomUsers = normalizeMatrixAllowList(params.roomUsers); - const commandAllowFrom = params.isRoom ? configuredAllowFrom : effectiveAllowFrom; + const commandAllowFrom = params.isRoom ? [] : effectiveAllowFrom; const directAllowMatch = resolveMatrixAllowListMatch({ allowList: effectiveAllowFrom, diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index b46b6ed60a4..1c6d0ba245f 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -512,6 +512,45 @@ describe("matrix monitor handler pairing account scope", () => { expect(readAllowFromStore).not.toHaveBeenCalled(); }); + it("blocks room control commands from configured DM-only senders", async () => { + const hasControlCommand = vi.fn((text?: string) => text === "/new"); + const { handler, finalizeInboundContext, recordInboundSession } = + createMatrixHandlerTestHarness({ + isDirectMessage: false, + roomsConfig: { + "!room:example.org": { requireMention: false }, + }, + shouldHandleTextCommands: () => true, + hasControlCommand, + cfg: { + commands: { + useAccessGroups: true, + }, + channels: { + matrix: { + dm: { allowFrom: ["@observer:example.org"] }, + groupAllowFrom: ["@driver:example.org"], + }, + }, + }, + groupPolicy: "open", + getMemberDisplayName: async () => "observer", + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$dm-configured-room-command", + sender: "@observer:example.org", + body: "@bot:example.org /new", + }), + ); + + expect(hasControlCommand).toHaveBeenCalledWith("/new", expect.anything()); + expect(recordInboundSession).not.toHaveBeenCalled(); + expect(finalizeInboundContext).not.toHaveBeenCalled(); + }); + it("strips the Matrix self user id before room slash command detection", async () => { const hasControlCommand = vi.fn((text?: string) => text === "/new"); const { handler, finalizeInboundContext, recordInboundSession } =