From f8705f512b09043df02b5da372c33374734bd921 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Gondhi Date: Wed, 15 Apr 2026 22:45:14 +0530 Subject: [PATCH] fix(matrix): block DM pairing-store entries from authorizing room control commands [AI-assisted] (#67294) * fix: address issue * fix: address review feedback * docs: add changelog entry for PR merge --- CHANGELOG.md | 1 + .../src/matrix/monitor/access-state.test.ts | 19 ++++++++++++ .../matrix/src/matrix/monitor/access-state.ts | 28 ++++++++++++----- .../matrix/src/matrix/monitor/handler.test.ts | 30 +++++++++++++++++++ 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de167ff0be4..f59d770c33f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- fix(matrix): block DM pairing-store entries from authorizing room control commands [AI-assisted]. (#67294) Thanks @pgondhi987. - Docker/build: verify `@matrix-org/matrix-sdk-crypto-nodejs` native bindings with `find` under `node_modules` instead of a hardcoded `.pnpm/...` path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559. - Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring `channels.matrix.password`, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792. - Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with `NO_REPLY` so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator. diff --git a/extensions/matrix/src/matrix/monitor/access-state.test.ts b/extensions/matrix/src/matrix/monitor/access-state.test.ts index 46f22e2c957..b51e45d2e9b 100644 --- a/extensions/matrix/src/matrix/monitor/access-state.test.ts +++ b/extensions/matrix/src/matrix/monitor/access-state.test.ts @@ -28,6 +28,25 @@ describe("resolveMatrixMonitorAccessState", () => { ]); }); + it("does not let DM pairing-store entries authorize room control commands", () => { + const state = resolveMatrixMonitorAccessState({ + allowFrom: [], + storeAllowFrom: ["@attacker:example.org"], + groupAllowFrom: [], + roomUsers: [], + senderId: "@attacker:example.org", + isRoom: true, + }); + + expect(state.effectiveAllowFrom).toEqual(["@attacker:example.org"]); + expect(state.directAllowMatch.allowed).toBe(true); + expect(state.commandAuthorizers).toEqual([ + { configured: false, allowed: false }, + { configured: false, allowed: false }, + { configured: false, 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 8677b57d749..586319420a9 100644 --- a/extensions/matrix/src/matrix/monitor/access-state.ts +++ b/extensions/matrix/src/matrix/monitor/access-state.ts @@ -1,19 +1,24 @@ import { normalizeMatrixAllowList, resolveMatrixAllowListMatch } from "./allowlist.js"; -import type { MatrixAllowListMatch } from "./allowlist.js"; type MatrixCommandAuthorizer = { configured: boolean; allowed: boolean; }; +type MatrixMonitorAllowListMatch = { + allowed: boolean; + matchKey?: string; + matchSource?: "wildcard" | "id" | "prefixed-id" | "prefixed-user"; +}; + export type MatrixMonitorAccessState = { effectiveAllowFrom: string[]; effectiveGroupAllowFrom: string[]; effectiveRoomUsers: string[]; groupAllowConfigured: boolean; - directAllowMatch: MatrixAllowListMatch; - roomUserMatch: MatrixAllowListMatch | null; - groupAllowMatch: MatrixAllowListMatch | null; + directAllowMatch: MatrixMonitorAllowListMatch; + roomUserMatch: MatrixMonitorAllowListMatch | null; + groupAllowMatch: MatrixMonitorAllowListMatch | null; commandAuthorizers: [MatrixCommandAuthorizer, MatrixCommandAuthorizer, MatrixCommandAuthorizer]; }; @@ -25,12 +30,14 @@ export function resolveMatrixMonitorAccessState(params: { senderId: string; isRoom: boolean; }): MatrixMonitorAccessState { + const configuredAllowFrom = normalizeMatrixAllowList(params.allowFrom); const effectiveAllowFrom = normalizeMatrixAllowList([ - ...params.allowFrom, + ...configuredAllowFrom, ...params.storeAllowFrom, ]); const effectiveGroupAllowFrom = normalizeMatrixAllowList(params.groupAllowFrom); const effectiveRoomUsers = normalizeMatrixAllowList(params.roomUsers); + const commandAllowFrom = params.isRoom ? configuredAllowFrom : effectiveAllowFrom; const directAllowMatch = resolveMatrixAllowListMatch({ allowList: effectiveAllowFrom, @@ -50,6 +57,13 @@ export function resolveMatrixMonitorAccessState(params: { userId: params.senderId, }) : null; + const commandAllowMatch = + commandAllowFrom.length > 0 + ? resolveMatrixAllowListMatch({ + allowList: commandAllowFrom, + userId: params.senderId, + }) + : null; return { effectiveAllowFrom, @@ -61,8 +75,8 @@ export function resolveMatrixMonitorAccessState(params: { groupAllowMatch, commandAuthorizers: [ { - configured: effectiveAllowFrom.length > 0, - allowed: directAllowMatch.allowed, + configured: commandAllowFrom.length > 0, + allowed: commandAllowMatch?.allowed ?? false, }, { configured: effectiveRoomUsers.length > 0, diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index d79a9ef8a49..9599c87008d 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -445,6 +445,36 @@ describe("matrix monitor handler pairing account scope", () => { expect(recordInboundSession).not.toHaveBeenCalled(); }); + it("blocks room control commands from DM-only paired senders", async () => { + const { handler, finalizeInboundContext, recordInboundSession } = + createMatrixHandlerTestHarness({ + isDirectMessage: false, + readAllowFromStore: vi.fn(async () => ["@user:example.org"]), + roomsConfig: { + "!room:example.org": { requireMention: false }, + }, + shouldHandleTextCommands: () => true, + hasControlCommand: () => true, + cfg: { + commands: { + useAccessGroups: true, + }, + }, + getMemberDisplayName: async () => "sender", + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$dm-only-room-command", + body: "/config", + }), + ); + + expect(recordInboundSession).not.toHaveBeenCalled(); + expect(finalizeInboundContext).not.toHaveBeenCalled(); + }); + it("processes room messages mentioned via displayName in formatted_body", async () => { const recordInboundSession = vi.fn(async () => {}); const { handler } = createMatrixHandlerTestHarness({