mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Matrix: harden alias trust and log redaction
This commit is contained in:
@@ -258,6 +258,7 @@ describe("matrix directory", () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.',
|
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.',
|
||||||
|
'- Matrix invites: autoJoin="always" joins any invited room before message policy applies. Set channels.matrix.autoJoin="allowlist" + channels.matrix.autoJoinAllowlist (or channels.matrix.autoJoin="off") to restrict joins.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -292,6 +293,33 @@ describe("matrix directory", () => {
|
|||||||
}),
|
}),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.accounts.assistant.groupPolicy="allowlist" + channels.matrix.accounts.assistant.groups (and optionally channels.matrix.accounts.assistant.groupAllowFrom) to restrict rooms.',
|
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.accounts.assistant.groupPolicy="allowlist" + channels.matrix.accounts.assistant.groups (and optionally channels.matrix.accounts.assistant.groupAllowFrom) to restrict rooms.',
|
||||||
|
'- Matrix invites: autoJoin="always" joins any invited room before message policy applies. Set channels.matrix.accounts.assistant.autoJoin="allowlist" + channels.matrix.accounts.assistant.autoJoinAllowlist (or channels.matrix.accounts.assistant.autoJoin="off") to restrict joins.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports invite auto-join warnings even when room policy is restricted", () => {
|
||||||
|
expect(
|
||||||
|
matrixPlugin.security?.collectWarnings?.({
|
||||||
|
cfg: {
|
||||||
|
channels: {
|
||||||
|
matrix: {
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as CoreConfig,
|
||||||
|
account: resolveMatrixAccount({
|
||||||
|
cfg: {
|
||||||
|
channels: {
|
||||||
|
matrix: {
|
||||||
|
groupPolicy: "allowlist",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as CoreConfig,
|
||||||
|
accountId: "default",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
).toEqual([
|
||||||
|
'- Matrix invites: autoJoin="always" joins any invited room before message policy applies. Set channels.matrix.autoJoin="allowlist" + channels.matrix.autoJoinAllowlist (or channels.matrix.autoJoin="off") to restrict joins.',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -158,13 +158,19 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|||||||
groupPolicy: account.config.groupPolicy,
|
groupPolicy: account.config.groupPolicy,
|
||||||
defaultGroupPolicy,
|
defaultGroupPolicy,
|
||||||
});
|
});
|
||||||
if (groupPolicy !== "open") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const configPath = resolveMatrixConfigPath(cfg as CoreConfig, account.accountId);
|
const configPath = resolveMatrixConfigPath(cfg as CoreConfig, account.accountId);
|
||||||
return [
|
const warnings: string[] = [];
|
||||||
`- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set ${configPath}.groupPolicy="allowlist" + ${configPath}.groups (and optionally ${configPath}.groupAllowFrom) to restrict rooms.`,
|
if (groupPolicy === "open") {
|
||||||
];
|
warnings.push(
|
||||||
|
`- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set ${configPath}.groupPolicy="allowlist" + ${configPath}.groups (and optionally ${configPath}.groupAllowFrom) to restrict rooms.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ((account.config.autoJoin ?? "always") === "always") {
|
||||||
|
warnings.push(
|
||||||
|
`- Matrix invites: autoJoin="always" joins any invited room before message policy applies. Set ${configPath}.autoJoin="allowlist" + ${configPath}.autoJoinAllowlist (or ${configPath}.autoJoin="off") to restrict joins.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
|
|||||||
@@ -16,15 +16,14 @@ function createClientStub() {
|
|||||||
return client;
|
return client;
|
||||||
}),
|
}),
|
||||||
joinRoom: vi.fn(async () => {}),
|
joinRoom: vi.fn(async () => {}),
|
||||||
getRoomStateEvent: vi.fn(async () => ({})),
|
resolveRoom: vi.fn(async () => null),
|
||||||
} as unknown as import("../sdk.js").MatrixClient;
|
} as unknown as import("../sdk.js").MatrixClient;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client,
|
client,
|
||||||
getInviteHandler: () => inviteHandler,
|
getInviteHandler: () => inviteHandler,
|
||||||
joinRoom: (client as unknown as { joinRoom: ReturnType<typeof vi.fn> }).joinRoom,
|
joinRoom: (client as unknown as { joinRoom: ReturnType<typeof vi.fn> }).joinRoom,
|
||||||
getRoomStateEvent: (client as unknown as { getRoomStateEvent: ReturnType<typeof vi.fn> })
|
resolveRoom: (client as unknown as { resolveRoom: ReturnType<typeof vi.fn> }).resolveRoom,
|
||||||
.getRoomStateEvent,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +59,8 @@ describe("registerMatrixAutoJoin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("ignores invites outside allowlist when autoJoin=allowlist", async () => {
|
it("ignores invites outside allowlist when autoJoin=allowlist", async () => {
|
||||||
const { client, getInviteHandler, joinRoom, getRoomStateEvent } = createClientStub();
|
const { client, getInviteHandler, joinRoom, resolveRoom } = createClientStub();
|
||||||
getRoomStateEvent.mockResolvedValue({
|
resolveRoom.mockResolvedValue(null);
|
||||||
alias: "#other:example.org",
|
|
||||||
alt_aliases: ["#else:example.org"],
|
|
||||||
});
|
|
||||||
const accountConfig: MatrixConfig = {
|
const accountConfig: MatrixConfig = {
|
||||||
autoJoin: "allowlist",
|
autoJoin: "allowlist",
|
||||||
autoJoinAllowlist: ["#allowed:example.org"],
|
autoJoinAllowlist: ["#allowed:example.org"],
|
||||||
@@ -86,12 +82,9 @@ describe("registerMatrixAutoJoin", () => {
|
|||||||
expect(joinRoom).not.toHaveBeenCalled();
|
expect(joinRoom).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("joins invite when alias matches allowlist", async () => {
|
it("joins invite when allowlisted alias resolves to the invited room", async () => {
|
||||||
const { client, getInviteHandler, joinRoom, getRoomStateEvent } = createClientStub();
|
const { client, getInviteHandler, joinRoom, resolveRoom } = createClientStub();
|
||||||
getRoomStateEvent.mockResolvedValue({
|
resolveRoom.mockResolvedValue("!room:example.org");
|
||||||
alias: "#allowed:example.org",
|
|
||||||
alt_aliases: ["#backup:example.org"],
|
|
||||||
});
|
|
||||||
const accountConfig: MatrixConfig = {
|
const accountConfig: MatrixConfig = {
|
||||||
autoJoin: "allowlist",
|
autoJoin: "allowlist",
|
||||||
autoJoinAllowlist: [" #allowed:example.org "],
|
autoJoinAllowlist: [" #allowed:example.org "],
|
||||||
@@ -113,13 +106,33 @@ describe("registerMatrixAutoJoin", () => {
|
|||||||
expect(joinRoom).toHaveBeenCalledWith("!room:example.org");
|
expect(joinRoom).toHaveBeenCalledWith("!room:example.org");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses account-scoped auto-join settings for non-default accounts", async () => {
|
it("does not trust room-provided alias claims for allowlist joins", async () => {
|
||||||
const { client, getInviteHandler, joinRoom, getRoomStateEvent } = createClientStub();
|
const { client, getInviteHandler, joinRoom, resolveRoom } = createClientStub();
|
||||||
getRoomStateEvent.mockResolvedValue({
|
resolveRoom.mockResolvedValue("!different-room:example.org");
|
||||||
alias: "#ops-allowed:example.org",
|
|
||||||
alt_aliases: [],
|
registerMatrixAutoJoin({
|
||||||
|
client,
|
||||||
|
accountConfig: {
|
||||||
|
autoJoin: "allowlist",
|
||||||
|
autoJoinAllowlist: ["#allowed:example.org"],
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
} as unknown as import("openclaw/plugin-sdk/matrix").RuntimeEnv,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inviteHandler = getInviteHandler();
|
||||||
|
expect(inviteHandler).toBeTruthy();
|
||||||
|
await inviteHandler!("!room:example.org", {});
|
||||||
|
|
||||||
|
expect(joinRoom).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses account-scoped auto-join settings for non-default accounts", async () => {
|
||||||
|
const { client, getInviteHandler, joinRoom, resolveRoom } = createClientStub();
|
||||||
|
resolveRoom.mockResolvedValue("!room:example.org");
|
||||||
|
|
||||||
registerMatrixAutoJoin({
|
registerMatrixAutoJoin({
|
||||||
client,
|
client,
|
||||||
accountConfig: {
|
accountConfig: {
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ export function registerMatrixAutoJoin(params: {
|
|||||||
runtime.log?.(message);
|
runtime.log?.(message);
|
||||||
};
|
};
|
||||||
const autoJoin = accountConfig.autoJoin ?? "always";
|
const autoJoin = accountConfig.autoJoin ?? "always";
|
||||||
const autoJoinAllowlist = new Set(
|
const rawAllowlist = (accountConfig.autoJoinAllowlist ?? [])
|
||||||
(accountConfig.autoJoinAllowlist ?? []).map((entry) => String(entry).trim()).filter(Boolean),
|
.map((entry) => String(entry).trim())
|
||||||
);
|
.filter(Boolean);
|
||||||
|
const autoJoinAllowlist = new Set(rawAllowlist);
|
||||||
|
const allowedRoomIds = new Set(rawAllowlist.filter((entry) => entry.startsWith("!")));
|
||||||
|
const allowedAliases = rawAllowlist.filter((entry) => entry.startsWith("#"));
|
||||||
|
const resolvedAliasRoomIds = new Map<string, string | null>();
|
||||||
|
|
||||||
if (autoJoin === "off") {
|
if (autoJoin === "off") {
|
||||||
return;
|
return;
|
||||||
@@ -31,31 +35,25 @@ export function registerMatrixAutoJoin(params: {
|
|||||||
logVerbose("matrix: auto-join enabled for allowlist invites");
|
logVerbose("matrix: auto-join enabled for allowlist invites");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveAllowedAliasRoomId = async (alias: string): Promise<string | null> => {
|
||||||
|
if (resolvedAliasRoomIds.has(alias)) {
|
||||||
|
return resolvedAliasRoomIds.get(alias) ?? null;
|
||||||
|
}
|
||||||
|
const resolved = await params.client.resolveRoom(alias);
|
||||||
|
resolvedAliasRoomIds.set(alias, resolved);
|
||||||
|
return resolved;
|
||||||
|
};
|
||||||
|
|
||||||
// Handle invites directly so both "always" and "allowlist" modes share the same path.
|
// Handle invites directly so both "always" and "allowlist" modes share the same path.
|
||||||
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
|
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
|
||||||
if (autoJoin === "allowlist") {
|
if (autoJoin === "allowlist") {
|
||||||
let alias: string | undefined;
|
const allowedAliasRoomIds = await Promise.all(
|
||||||
let altAliases: string[] = [];
|
allowedAliases.map(async (alias) => await resolveAllowedAliasRoomId(alias)),
|
||||||
try {
|
);
|
||||||
const aliasState = await client
|
|
||||||
.getRoomStateEvent(roomId, "m.room.canonical_alias", "")
|
|
||||||
.catch(() => null);
|
|
||||||
alias = aliasState && typeof aliasState.alias === "string" ? aliasState.alias : undefined;
|
|
||||||
altAliases =
|
|
||||||
aliasState && Array.isArray(aliasState.alt_aliases)
|
|
||||||
? aliasState.alt_aliases
|
|
||||||
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowed =
|
const allowed =
|
||||||
autoJoinAllowlist.has("*") ||
|
autoJoinAllowlist.has("*") ||
|
||||||
autoJoinAllowlist.has(roomId) ||
|
allowedRoomIds.has(roomId) ||
|
||||||
(alias ? autoJoinAllowlist.has(alias) : false) ||
|
allowedAliasRoomIds.some((resolvedRoomId) => resolvedRoomId === roomId);
|
||||||
altAliases.some((value) => autoJoinAllowlist.has(value));
|
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`);
|
logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`);
|
||||||
|
|||||||
@@ -153,4 +153,45 @@ describe("resolveMatrixMonitorConfig", () => {
|
|||||||
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves exact room aliases to canonical room ids instead of trusting alias keys directly", async () => {
|
||||||
|
const runtime = createRuntime();
|
||||||
|
const resolveTargets = vi.fn(
|
||||||
|
async ({ kind, inputs }: { inputs: string[]; kind: "user" | "group" }) => {
|
||||||
|
if (kind === "group") {
|
||||||
|
return inputs.map((input) =>
|
||||||
|
input === "#allowed:example.org"
|
||||||
|
? { input, resolved: true, id: "!allowed-room:example.org" }
|
||||||
|
: { input, resolved: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await resolveMatrixMonitorConfig({
|
||||||
|
cfg: {} as CoreConfig,
|
||||||
|
accountId: "ops",
|
||||||
|
roomsConfig: {
|
||||||
|
"#allowed:example.org": {
|
||||||
|
allow: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
resolveTargets,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.roomsConfig).toEqual({
|
||||||
|
"!allowed-room:example.org": {
|
||||||
|
allow: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(resolveTargets).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
accountId: "ops",
|
||||||
|
kind: "group",
|
||||||
|
inputs: ["#allowed:example.org"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ async function resolveMatrixMonitorRoomsConfig(params: {
|
|||||||
unresolved.push(entry);
|
unresolved.push(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) {
|
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
||||||
if (!nextRooms[cleaned]) {
|
if (!nextRooms[cleaned]) {
|
||||||
nextRooms[cleaned] = roomConfig;
|
nextRooms[cleaned] = roomConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
25
extensions/matrix/src/matrix/sdk/logger.test.ts
Normal file
25
extensions/matrix/src/matrix/sdk/logger.test.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { ConsoleLogger, setMatrixConsoleLogging } from "./logger.js";
|
||||||
|
|
||||||
|
describe("ConsoleLogger", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
setMatrixConsoleLogging(false);
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts sensitive tokens in emitted log messages", () => {
|
||||||
|
setMatrixConsoleLogging(true);
|
||||||
|
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
|
||||||
|
new ConsoleLogger().error(
|
||||||
|
"MatrixHttpClient",
|
||||||
|
"Authorization: Bearer 123456:abcdefghijklmnopqrstuvwxyzABCDEFG",
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = spy.mock.calls[0]?.[0];
|
||||||
|
expect(typeof message).toBe("string");
|
||||||
|
expect(message).toContain("Authorization: Bearer");
|
||||||
|
expect(message).not.toContain("123456:abcdefghijklmnopqrstuvwxyzABCDEFG");
|
||||||
|
expect(message).toContain("***");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { format } from "node:util";
|
import { format } from "node:util";
|
||||||
import type { RuntimeLogger } from "openclaw/plugin-sdk/matrix";
|
import { redactSensitiveText, type RuntimeLogger } from "openclaw/plugin-sdk/matrix";
|
||||||
import { getMatrixRuntime } from "../../runtime.js";
|
import { getMatrixRuntime } from "../../runtime.js";
|
||||||
|
|
||||||
export type Logger = {
|
export type Logger = {
|
||||||
@@ -35,7 +35,7 @@ function formatMessage(module: string, messageOrObject: unknown[]): string {
|
|||||||
if (messageOrObject.length === 0) {
|
if (messageOrObject.length === 0) {
|
||||||
return `[${module}]`;
|
return `[${module}]`;
|
||||||
}
|
}
|
||||||
return `[${module}] ${format(...messageOrObject)}`;
|
return redactSensitiveText(`[${module}] ${format(...messageOrObject)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConsoleLogger {
|
export class ConsoleLogger {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ export {
|
|||||||
} from "../security/dm-policy-shared.js";
|
} from "../security/dm-policy-shared.js";
|
||||||
export { normalizeStringEntries } from "../shared/string-normalization.js";
|
export { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||||
export { formatDocsLink } from "../terminal/links.js";
|
export { formatDocsLink } from "../terminal/links.js";
|
||||||
|
export { redactSensitiveText } from "../logging/redact.js";
|
||||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
export {
|
export {
|
||||||
evaluateGroupRouteAccessForPolicy,
|
evaluateGroupRouteAccessForPolicy,
|
||||||
|
|||||||
Reference in New Issue
Block a user