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
This commit is contained in:
Pavan Kumar Gondhi
2026-04-15 22:45:14 +05:30
committed by GitHub
parent ed28df48a4
commit f8705f512b
4 changed files with 71 additions and 7 deletions

View File

@@ -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.

View File

@@ -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: [],

View File

@@ -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,

View File

@@ -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({