Compare commits

...

1 Commits

Author SHA1 Message Date
Gustavo Madeira Santana
f45bddf13d matrix: block DM-paired senders from room commands 2026-04-15 13:24:07 -04:00
4 changed files with 62 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Matrix/commands: keep DM pairing-store approvals out of room control-command authorization so DM-paired-only senders can no longer run owner-style commands in Matrix rooms without explicit room sender authorization.
- 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

@@ -25,12 +25,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 +52,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 +70,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({