VYPR
Moderate severityNVD Advisory· Published Mar 5, 2026· Updated Mar 9, 2026

OpenClaw 2026.1.14-1 < 2026.2.2 - Allowlist Bypass via displayName and Cross-Homeserver localpart Matching in Matrix Plugin

CVE-2026-28471

Description

OpenClaw version 2026.1.14-1 prior to 2026.2.2, with the Matrix plugin installed and enabled, contain a vulnerability in which DM allowlist matching could be bypassed by exact-matching against sender display names and localparts without homeserver validation. Remote Matrix users can impersonate allowed identities by using attacker-controlled display names or matching localparts from different homeservers to reach the routing and agent pipeline.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
>= 2026.1.14-1, < 2026.2.22026.2.2

Affected products

1

Patches

1
8f3bfbd1c4fb

fix(matrix): harden allowlists

https://github.com/openclaw/openclawPeter SteinbergerFeb 3, 2026via ghsa
13 files changed · +357 104
  • CHANGELOG.md+1 0 modified
    @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
     
     ### Fixes
     
    +- Security: Matrix allowlists now require full MXIDs; ambiguous name resolution no longer grants access. Thanks @MegaManSec.
     - Docs: finish renaming the QMD memory docs to reference the OpenClaw state dir.
     - Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed).
     - Onboarding: drop completion prompt now handled by install/update.
    
  • docs/channels/matrix.md+8 8 modified
    @@ -148,12 +148,12 @@ Once verified, the bot can decrypt messages in encrypted rooms.
       - `openclaw pairing list matrix`
       - `openclaw pairing approve matrix <CODE>`
     - Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`.
    -- `channels.matrix.dm.allowFrom` accepts user IDs or display names. The wizard resolves display names to user IDs when directory search is available.
    +- `channels.matrix.dm.allowFrom` accepts full Matrix user IDs (example: `@user:server`). The wizard resolves display names to user IDs when directory search finds a single exact match.
     
     ## Rooms (groups)
     
     - Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset.
    -- Allowlist rooms with `channels.matrix.groups` (room IDs, aliases, or names):
    +- Allowlist rooms with `channels.matrix.groups` (room IDs or aliases; names are resolved to IDs when directory search finds a single exact match):
     
     ```json5
     {
    @@ -172,10 +172,10 @@ Once verified, the bot can decrypt messages in encrypted rooms.
     
     - `requireMention: false` enables auto-reply in that room.
     - `groups."*"` can set defaults for mention gating across rooms.
    -- `groupAllowFrom` restricts which senders can trigger the bot in rooms (optional).
    -- Per-room `users` allowlists can further restrict senders inside a specific room.
    -- The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names when possible.
    -- On startup, OpenClaw resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
    +- `groupAllowFrom` restricts which senders can trigger the bot in rooms (full Matrix user IDs).
    +- Per-room `users` allowlists can further restrict senders inside a specific room (use full Matrix user IDs).
    +- The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names only on an exact, unique match.
    +- On startup, OpenClaw resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are ignored for allowlist matching.
     - Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`.
     - To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist).
     - Legacy key: `channels.matrix.rooms` (same shape as `groups`).
    @@ -220,9 +220,9 @@ Provider options:
     - `channels.matrix.textChunkLimit`: outbound text chunk size (chars).
     - `channels.matrix.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
     - `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing).
    -- `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible.
    +- `channels.matrix.dm.allowFrom`: DM allowlist (full Matrix user IDs). `open` requires `"*"`. The wizard resolves names to IDs when possible.
     - `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist).
    -- `channels.matrix.groupAllowFrom`: allowlisted senders for group messages.
    +- `channels.matrix.groupAllowFrom`: allowlisted senders for group messages (full Matrix user IDs).
     - `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms.
     - `channels.matrix.groups`: group allowlist + per-room settings map.
     - `channels.matrix.rooms`: legacy group allowlist/config.
    
  • docs/zh-CN/channels/matrix.md+8 8 modified
    @@ -136,12 +136,12 @@ openclaw plugins install ./extensions/matrix
       - `openclaw pairing list matrix`
       - `openclaw pairing approve matrix <CODE>`
     - 公开私信:`channels.matrix.dm.policy="open"` 加上 `channels.matrix.dm.allowFrom=["*"]`。
    -- `channels.matrix.dm.allowFrom` 接受用户 ID 或显示名称。向导在目录搜索可用时会将显示名称解析为用户 ID。
    +- `channels.matrix.dm.allowFrom` 仅接受完整 Matrix 用户 ID(例如 `@user:server`)。向导仅在目录搜索得到唯一精确匹配时解析显示名称为用户 ID。
     
     ## 房间(群组)
     
     - 默认:`channels.matrix.groupPolicy = "allowlist"`(提及门控)。使用 `channels.defaults.groupPolicy` 可在未设置时覆盖默认值。
    -- 使用 `channels.matrix.groups` 允许列表中的房间(房间 ID、别名或名称):
    +- 使用 `channels.matrix.groups` 允许列表中的房间(房间 ID/别名;名称仅在目录搜索得到唯一精确匹配时解析为 ID):
     
     ```json5
     {
    @@ -160,10 +160,10 @@ openclaw plugins install ./extensions/matrix
     
     - `requireMention: false` 启用该房间的自动回复。
     - `groups."*"` 可以设置跨房间的提及门控默认值。
    -- `groupAllowFrom` 限制哪些发送者可以在房间中触发机器人(可选)。
    -- 按房间的 `users` 允许列表可以进一步限制特定房间内的发送者。
    -- 配置向导会提示输入房间允许列表(房间 ID、别名或名称)并在可能时解析名称。
    -- 启动时,OpenClaw 将允许列表中的房间/用户名称解析为 ID 并记录映射;未解析的条目保持原样。
    +- `groupAllowFrom` 限制哪些发送者可以在房间中触发机器人(完整 Matrix 用户 ID)。
    +- 按房间的 `users` 允许列表可以进一步限制特定房间内的发送者(使用完整 Matrix 用户 ID)。
    +- 配置向导会提示输入房间允许列表(房间 ID、别名或名称),仅在精确且唯一匹配时解析名称。
    +- 启动时,OpenClaw 将允许列表中的房间/用户名称解析为 ID 并记录映射;未解析的条目不会参与允许列表匹配。
     - 邀请默认自动加入;通过 `channels.matrix.autoJoin` 和 `channels.matrix.autoJoinAllowlist` 控制。
     - 要**不允许任何房间**,设置 `channels.matrix.groupPolicy: "disabled"`(或保持空的允许列表)。
     - 旧版键:`channels.matrix.rooms`(与 `groups` 结构相同)。
    @@ -208,9 +208,9 @@ openclaw plugins install ./extensions/matrix
     - `channels.matrix.textChunkLimit`:出站文本分块大小(字符)。
     - `channels.matrix.chunkMode`:`length`(默认)或 `newline`,在按长度分块之前按空行(段落边界)分割。
     - `channels.matrix.dm.policy`:`pairing | allowlist | open | disabled`(默认:pairing)。
    -- `channels.matrix.dm.allowFrom`:私信允许列表(用户 ID 或显示名称)。`open` 需要 `"*"`。向导在可能时将名称解析为 ID。
    +- `channels.matrix.dm.allowFrom`:私信允许列表(完整 Matrix 用户 ID)。`open` 需要 `"*"`。向导在可能时将名称解析为 ID。
     - `channels.matrix.groupPolicy`:`allowlist | open | disabled`(默认:allowlist)。
    -- `channels.matrix.groupAllowFrom`:群组消息的允许发送者列表。
    +- `channels.matrix.groupAllowFrom`:群组消息的允许发送者列表(完整 Matrix 用户 ID)。
     - `channels.matrix.allowlistOnly`:强制对私信 + 房间执行允许列表规则。
     - `channels.matrix.groups`:群组允许列表 + 按房间设置映射。
     - `channels.matrix.rooms`:旧版群组允许列表/配置。
    
  • extensions/matrix/src/channel.ts+3 7 modified
    @@ -24,7 +24,7 @@ import {
       type ResolvedMatrixAccount,
     } from "./matrix/accounts.js";
     import { resolveMatrixAuth } from "./matrix/client.js";
    -import { normalizeAllowListLower } from "./matrix/monitor/allowlist.js";
    +import { normalizeMatrixAllowList, normalizeMatrixUserId } from "./matrix/monitor/allowlist.js";
     import { probeMatrix } from "./matrix/probe.js";
     import { sendMessageMatrix } from "./matrix/send.js";
     import { matrixOnboardingAdapter } from "./onboarding.js";
    @@ -144,7 +144,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
         }),
         resolveAllowFrom: ({ cfg }) =>
           ((cfg as CoreConfig).channels?.matrix?.dm?.allowFrom ?? []).map((entry) => String(entry)),
    -    formatAllowFrom: ({ allowFrom }) => normalizeAllowListLower(allowFrom),
    +    formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom),
       },
       security: {
         resolveDmPolicy: ({ account }) => ({
    @@ -153,11 +153,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
           policyPath: "channels.matrix.dm.policy",
           allowFromPath: "channels.matrix.dm.allowFrom",
           approveHint: formatPairingApproveHint("matrix"),
    -      normalizeEntry: (raw) =>
    -        raw
    -          .replace(/^matrix:/i, "")
    -          .trim()
    -          .toLowerCase(),
    +      normalizeEntry: (raw) => normalizeMatrixUserId(raw),
         }),
         collectWarnings: ({ account, cfg }) => {
           const defaultGroupPolicy = (cfg as CoreConfig).channels?.defaults?.groupPolicy;
    
  • extensions/matrix/src/matrix/monitor/allowlist.test.ts+45 0 added
    @@ -0,0 +1,45 @@
    +import { describe, expect, it } from "vitest";
    +import { normalizeMatrixAllowList, resolveMatrixAllowListMatch } from "./allowlist.js";
    +
    +describe("resolveMatrixAllowListMatch", () => {
    +  it("matches full user IDs and prefixes", () => {
    +    const userId = "@Alice:Example.org";
    +    const direct = resolveMatrixAllowListMatch({
    +      allowList: normalizeMatrixAllowList(["@alice:example.org"]),
    +      userId,
    +    });
    +    expect(direct.allowed).toBe(true);
    +    expect(direct.matchSource).toBe("id");
    +
    +    const prefixedMatrix = resolveMatrixAllowListMatch({
    +      allowList: normalizeMatrixAllowList(["matrix:@alice:example.org"]),
    +      userId,
    +    });
    +    expect(prefixedMatrix.allowed).toBe(true);
    +    expect(prefixedMatrix.matchSource).toBe("prefixed-id");
    +
    +    const prefixedUser = resolveMatrixAllowListMatch({
    +      allowList: normalizeMatrixAllowList(["user:@alice:example.org"]),
    +      userId,
    +    });
    +    expect(prefixedUser.allowed).toBe(true);
    +    expect(prefixedUser.matchSource).toBe("prefixed-user");
    +  });
    +
    +  it("ignores display names and localparts", () => {
    +    const match = resolveMatrixAllowListMatch({
    +      allowList: normalizeMatrixAllowList(["alice", "Alice"]),
    +      userId: "@alice:example.org",
    +    });
    +    expect(match.allowed).toBe(false);
    +  });
    +
    +  it("matches wildcard", () => {
    +    const match = resolveMatrixAllowListMatch({
    +      allowList: normalizeMatrixAllowList(["*"]),
    +      userId: "@alice:example.org",
    +    });
    +    expect(match.allowed).toBe(true);
    +    expect(match.matchSource).toBe("wildcard");
    +  });
    +});
    
  • extensions/matrix/src/matrix/monitor/allowlist.ts+56 15 modified
    @@ -4,22 +4,71 @@ function normalizeAllowList(list?: Array<string | number>) {
       return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
     }
     
    -export function normalizeAllowListLower(list?: Array<string | number>) {
    -  return normalizeAllowList(list).map((entry) => entry.toLowerCase());
    +function normalizeMatrixUser(raw?: string | null): string {
    +  const value = (raw ?? "").trim();
    +  if (!value) {
    +    return "";
    +  }
    +  if (!value.startsWith("@") || !value.includes(":")) {
    +    return value.toLowerCase();
    +  }
    +  const withoutAt = value.slice(1);
    +  const splitIndex = withoutAt.indexOf(":");
    +  if (splitIndex === -1) {
    +    return value.toLowerCase();
    +  }
    +  const localpart = withoutAt.slice(0, splitIndex).toLowerCase();
    +  const server = withoutAt.slice(splitIndex + 1).toLowerCase();
    +  if (!server) {
    +    return value.toLowerCase();
    +  }
    +  return `@${localpart}:${server.toLowerCase()}`;
     }
     
    -function normalizeMatrixUser(raw?: string | null): string {
    -  return (raw ?? "").trim().toLowerCase();
    +export function normalizeMatrixUserId(raw?: string | null): string {
    +  const trimmed = (raw ?? "").trim();
    +  if (!trimmed) {
    +    return "";
    +  }
    +  const lowered = trimmed.toLowerCase();
    +  if (lowered.startsWith("matrix:")) {
    +    return normalizeMatrixUser(trimmed.slice("matrix:".length));
    +  }
    +  if (lowered.startsWith("user:")) {
    +    return normalizeMatrixUser(trimmed.slice("user:".length));
    +  }
    +  return normalizeMatrixUser(trimmed);
    +}
    +
    +function normalizeMatrixAllowListEntry(raw: string): string {
    +  const trimmed = raw.trim();
    +  if (!trimmed) {
    +    return "";
    +  }
    +  if (trimmed === "*") {
    +    return trimmed;
    +  }
    +  const lowered = trimmed.toLowerCase();
    +  if (lowered.startsWith("matrix:")) {
    +    return `matrix:${normalizeMatrixUser(trimmed.slice("matrix:".length))}`;
    +  }
    +  if (lowered.startsWith("user:")) {
    +    return `user:${normalizeMatrixUser(trimmed.slice("user:".length))}`;
    +  }
    +  return normalizeMatrixUser(trimmed);
    +}
    +
    +export function normalizeMatrixAllowList(list?: Array<string | number>) {
    +  return normalizeAllowList(list).map((entry) => normalizeMatrixAllowListEntry(entry));
     }
     
     export type MatrixAllowListMatch = AllowlistMatch<
    -  "wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart"
    +  "wildcard" | "id" | "prefixed-id" | "prefixed-user"
     >;
     
     export function resolveMatrixAllowListMatch(params: {
       allowList: string[];
       userId?: string;
    -  userName?: string;
     }): MatrixAllowListMatch {
       const allowList = params.allowList;
       if (allowList.length === 0) {
    @@ -29,14 +78,10 @@ export function resolveMatrixAllowListMatch(params: {
         return { allowed: true, matchKey: "*", matchSource: "wildcard" };
       }
       const userId = normalizeMatrixUser(params.userId);
    -  const userName = normalizeMatrixUser(params.userName);
    -  const localPart = userId.startsWith("@") ? (userId.slice(1).split(":")[0] ?? "") : "";
       const candidates: Array<{ value?: string; source: MatrixAllowListMatch["matchSource"] }> = [
         { value: userId, source: "id" },
         { value: userId ? `matrix:${userId}` : "", source: "prefixed-id" },
         { value: userId ? `user:${userId}` : "", source: "prefixed-user" },
    -    { value: userName, source: "name" },
    -    { value: localPart, source: "localpart" },
       ];
       for (const candidate of candidates) {
         if (!candidate.value) {
    @@ -53,10 +98,6 @@ export function resolveMatrixAllowListMatch(params: {
       return { allowed: false };
     }
     
    -export function resolveMatrixAllowListMatches(params: {
    -  allowList: string[];
    -  userId?: string;
    -  userName?: string;
    -}) {
    +export function resolveMatrixAllowListMatches(params: { allowList: string[]; userId?: string }) {
       return resolveMatrixAllowListMatch(params).allowed;
     }
    
  • extensions/matrix/src/matrix/monitor/handler.ts+5 14 modified
    @@ -23,9 +23,9 @@ import {
       sendTypingMatrix,
     } from "../send.js";
     import {
    +  normalizeMatrixAllowList,
       resolveMatrixAllowListMatch,
       resolveMatrixAllowListMatches,
    -  normalizeAllowListLower,
     } from "./allowlist.js";
     import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
     import { downloadMatrixMedia } from "./media.js";
    @@ -236,12 +236,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
           const storeAllowFrom = await core.channel.pairing
             .readAllowFromStore("matrix")
             .catch(() => []);
    -      const effectiveAllowFrom = normalizeAllowListLower([...allowFrom, ...storeAllowFrom]);
    +      const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
           const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
    -      const effectiveGroupAllowFrom = normalizeAllowListLower([
    -        ...groupAllowFrom,
    -        ...storeAllowFrom,
    -      ]);
    +      const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
           const groupAllowConfigured = effectiveGroupAllowFrom.length > 0;
     
           if (isDirectMessage) {
    @@ -252,7 +249,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
               const allowMatch = resolveMatrixAllowListMatch({
                 allowList: effectiveAllowFrom,
                 userId: senderId,
    -            userName: senderName,
               });
               const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
               if (!allowMatch.allowed) {
    @@ -297,9 +293,8 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
           const roomUsers = roomConfig?.users ?? [];
           if (isRoom && roomUsers.length > 0) {
             const userMatch = resolveMatrixAllowListMatch({
    -          allowList: normalizeAllowListLower(roomUsers),
    +          allowList: normalizeMatrixAllowList(roomUsers),
               userId: senderId,
    -          userName: senderName,
             });
             if (!userMatch.allowed) {
               logVerboseMessage(
    @@ -314,7 +309,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
             const groupAllowMatch = resolveMatrixAllowListMatch({
               allowList: effectiveGroupAllowFrom,
               userId: senderId,
    -          userName: senderName,
             });
             if (!groupAllowMatch.allowed) {
               logVerboseMessage(
    @@ -387,21 +381,18 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
           const senderAllowedForCommands = resolveMatrixAllowListMatches({
             allowList: effectiveAllowFrom,
             userId: senderId,
    -        userName: senderName,
           });
           const senderAllowedForGroup = groupAllowConfigured
             ? resolveMatrixAllowListMatches({
                 allowList: effectiveGroupAllowFrom,
                 userId: senderId,
    -            userName: senderName,
               })
             : false;
           const senderAllowedForRoomUsers =
             isRoom && roomUsers.length > 0
               ? resolveMatrixAllowListMatches({
    -              allowList: normalizeAllowListLower(roomUsers),
    +              allowList: normalizeMatrixAllowList(roomUsers),
                   userId: senderId,
    -              userName: senderName,
                 })
               : false;
           const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
    
  • extensions/matrix/src/matrix/monitor/index.ts+91 44 modified
    @@ -10,6 +10,7 @@ import {
       resolveSharedMatrixClient,
       stopSharedClient,
     } from "../client.js";
    +import { normalizeMatrixUserId } from "./allowlist.js";
     import { registerMatrixAutoJoin } from "./auto-join.js";
     import { createDirectRoomTracker } from "./direct.js";
     import { registerMatrixMonitorEvents } from "./events.js";
    @@ -68,68 +69,94 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
           .replace(/^(room|channel):/i, "")
           .trim();
       const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":");
    -
    -  const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
    -  let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
    -  let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
    -
    -  if (allowFrom.length > 0) {
    -    const entries = allowFrom
    +  const resolveUserAllowlist = async (
    +    label: string,
    +    list?: Array<string | number>,
    +  ): Promise<string[]> => {
    +    let allowList = list ?? [];
    +    if (allowList.length === 0) {
    +      return allowList;
    +    }
    +    const entries = allowList
           .map((entry) => normalizeUserEntry(String(entry)))
           .filter((entry) => entry && entry !== "*");
    -    if (entries.length > 0) {
    -      const mapping: string[] = [];
    -      const unresolved: string[] = [];
    -      const additions: string[] = [];
    -      const pending: string[] = [];
    -      for (const entry of entries) {
    -        if (isMatrixUserId(entry)) {
    -          additions.push(entry);
    -          continue;
    -        }
    -        pending.push(entry);
    +    if (entries.length === 0) {
    +      return allowList;
    +    }
    +    const mapping: string[] = [];
    +    const unresolved: string[] = [];
    +    const additions: string[] = [];
    +    const pending: string[] = [];
    +    for (const entry of entries) {
    +      if (isMatrixUserId(entry)) {
    +        additions.push(normalizeMatrixUserId(entry));
    +        continue;
           }
    -      if (pending.length > 0) {
    -        const resolved = await resolveMatrixTargets({
    -          cfg,
    -          inputs: pending,
    -          kind: "user",
    -          runtime,
    -        });
    -        for (const entry of resolved) {
    -          if (entry.resolved && entry.id) {
    -            additions.push(entry.id);
    -            mapping.push(`${entry.input}→${entry.id}`);
    -          } else {
    -            unresolved.push(entry.input);
    -          }
    +      pending.push(entry);
    +    }
    +    if (pending.length > 0) {
    +      const resolved = await resolveMatrixTargets({
    +        cfg,
    +        inputs: pending,
    +        kind: "user",
    +        runtime,
    +      });
    +      for (const entry of resolved) {
    +        if (entry.resolved && entry.id) {
    +          const normalizedId = normalizeMatrixUserId(entry.id);
    +          additions.push(normalizedId);
    +          mapping.push(`${entry.input}→${normalizedId}`);
    +        } else {
    +          unresolved.push(entry.input);
             }
           }
    -      allowFrom = mergeAllowlist({ existing: allowFrom, additions });
    -      summarizeMapping("matrix users", mapping, unresolved, runtime);
         }
    -  }
    +    allowList = mergeAllowlist({ existing: allowList, additions });
    +    summarizeMapping(label, mapping, unresolved, runtime);
    +    if (unresolved.length > 0) {
    +      runtime.log?.(
    +        `${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
    +      );
    +    }
    +    return allowList;
    +  };
    +
    +  const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
    +  let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
    +  let groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
    +  let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
    +
    +  allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
    +  groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
     
       if (roomsConfig && Object.keys(roomsConfig).length > 0) {
    -    const entries = Object.keys(roomsConfig).filter((key) => key !== "*");
         const mapping: string[] = [];
         const unresolved: string[] = [];
    -    const nextRooms = { ...roomsConfig };
    -    const pending: Array<{ input: string; query: string }> = [];
    -    for (const entry of entries) {
    +    const nextRooms: Record<string, (typeof roomsConfig)[string]> = {};
    +    if (roomsConfig["*"]) {
    +      nextRooms["*"] = roomsConfig["*"];
    +    }
    +    const pending: Array<{ input: string; query: string; config: (typeof roomsConfig)[string] }> =
    +      [];
    +    for (const [entry, roomConfig] of Object.entries(roomsConfig)) {
    +      if (entry === "*") {
    +        continue;
    +      }
           const trimmed = entry.trim();
           if (!trimmed) {
             continue;
           }
           const cleaned = normalizeRoomEntry(trimmed);
    -      if (cleaned.startsWith("!") && cleaned.includes(":")) {
    +      if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) {
             if (!nextRooms[cleaned]) {
    -          nextRooms[cleaned] = roomsConfig[entry];
    +          nextRooms[cleaned] = roomConfig;
    +        }
    +        if (cleaned !== entry) {
    +          mapping.push(`${entry}→${cleaned}`);
             }
    -        mapping.push(`${entry}→${cleaned}`);
             continue;
           }
    -      pending.push({ input: entry, query: trimmed });
    +      pending.push({ input: entry, query: trimmed, config: roomConfig });
         }
         if (pending.length > 0) {
           const resolved = await resolveMatrixTargets({
    @@ -145,7 +172,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
             }
             if (entry.resolved && entry.id) {
               if (!nextRooms[entry.id]) {
    -            nextRooms[entry.id] = roomsConfig[source.input];
    +            nextRooms[entry.id] = source.config;
               }
               mapping.push(`${source.input}→${entry.id}`);
             } else {
    @@ -155,6 +182,25 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
         }
         roomsConfig = nextRooms;
         summarizeMapping("matrix rooms", mapping, unresolved, runtime);
    +    if (unresolved.length > 0) {
    +      runtime.log?.(
    +        "matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
    +      );
    +    }
    +  }
    +  if (roomsConfig && Object.keys(roomsConfig).length > 0) {
    +    const nextRooms = { ...roomsConfig };
    +    for (const [roomKey, roomConfig] of Object.entries(roomsConfig)) {
    +      const users = roomConfig?.users ?? [];
    +      if (users.length === 0) {
    +        continue;
    +      }
    +      const resolvedUsers = await resolveUserAllowlist(`matrix room users (${roomKey})`, users);
    +      if (resolvedUsers !== users) {
    +        nextRooms[roomKey] = { ...roomConfig, users: resolvedUsers };
    +      }
    +    }
    +    roomsConfig = nextRooms;
       }
     
       cfg = {
    @@ -167,6 +213,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
               ...cfg.channels?.matrix?.dm,
               allowFrom,
             },
    +        ...(groupAllowFrom.length > 0 ? { groupAllowFrom } : {}),
             ...(roomsConfig ? { groups: roomsConfig } : {}),
           },
         },
    
  • extensions/matrix/src/matrix/monitor/rooms.test.ts+39 0 added
    @@ -0,0 +1,39 @@
    +import { describe, expect, it } from "vitest";
    +import { resolveMatrixRoomConfig } from "./rooms.js";
    +
    +describe("resolveMatrixRoomConfig", () => {
    +  it("matches room IDs and aliases, not names", () => {
    +    const rooms = {
    +      "!room:example.org": { allow: true },
    +      "#alias:example.org": { allow: true },
    +      "Project Room": { allow: true },
    +    };
    +
    +    const byId = resolveMatrixRoomConfig({
    +      rooms,
    +      roomId: "!room:example.org",
    +      aliases: [],
    +      name: "Project Room",
    +    });
    +    expect(byId.allowed).toBe(true);
    +    expect(byId.matchKey).toBe("!room:example.org");
    +
    +    const byAlias = resolveMatrixRoomConfig({
    +      rooms,
    +      roomId: "!other:example.org",
    +      aliases: ["#alias:example.org"],
    +      name: "Other Room",
    +    });
    +    expect(byAlias.allowed).toBe(true);
    +    expect(byAlias.matchKey).toBe("#alias:example.org");
    +
    +    const byName = resolveMatrixRoomConfig({
    +      rooms: { "Project Room": { allow: true } },
    +      roomId: "!different:example.org",
    +      aliases: [],
    +      name: "Project Room",
    +    });
    +    expect(byName.allowed).toBe(false);
    +    expect(byName.config).toBeUndefined();
    +  });
    +});
    
  • extensions/matrix/src/matrix/monitor/rooms.ts+0 1 modified
    @@ -22,7 +22,6 @@ export function resolveMatrixRoomConfig(params: {
         params.roomId,
         `room:${params.roomId}`,
         ...params.aliases,
    -    params.name ?? "",
       );
       const {
         entry: matched,
    
  • extensions/matrix/src/resolve-targets.test.ts+48 0 added
    @@ -0,0 +1,48 @@
    +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
    +import { describe, expect, it, vi, beforeEach } from "vitest";
    +import { listMatrixDirectoryPeersLive } from "./directory-live.js";
    +import { resolveMatrixTargets } from "./resolve-targets.js";
    +
    +vi.mock("./directory-live.js", () => ({
    +  listMatrixDirectoryPeersLive: vi.fn(),
    +  listMatrixDirectoryGroupsLive: vi.fn(),
    +}));
    +
    +describe("resolveMatrixTargets (users)", () => {
    +  beforeEach(() => {
    +    vi.mocked(listMatrixDirectoryPeersLive).mockReset();
    +  });
    +
    +  it("resolves exact unique display name matches", async () => {
    +    const matches: ChannelDirectoryEntry[] = [
    +      { kind: "user", id: "@alice:example.org", name: "Alice" },
    +    ];
    +    vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
    +
    +    const [result] = await resolveMatrixTargets({
    +      cfg: {},
    +      inputs: ["Alice"],
    +      kind: "user",
    +    });
    +
    +    expect(result?.resolved).toBe(true);
    +    expect(result?.id).toBe("@alice:example.org");
    +  });
    +
    +  it("does not resolve ambiguous or non-exact matches", async () => {
    +    const matches: ChannelDirectoryEntry[] = [
    +      { kind: "user", id: "@alice:example.org", name: "Alice" },
    +      { kind: "user", id: "@alice:evil.example", name: "Alice" },
    +    ];
    +    vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
    +
    +    const [result] = await resolveMatrixTargets({
    +      cfg: {},
    +      inputs: ["Alice"],
    +      kind: "user",
    +    });
    +
    +    expect(result?.resolved).toBe(false);
    +    expect(result?.note).toMatch(/use full Matrix ID/i);
    +  });
    +});
    
  • extensions/matrix/src/resolve-targets.ts+48 2 modified
    @@ -28,6 +28,52 @@ function pickBestGroupMatch(
       return matches[0];
     }
     
    +function pickBestUserMatch(
    +  matches: ChannelDirectoryEntry[],
    +  query: string,
    +): ChannelDirectoryEntry | undefined {
    +  if (matches.length === 0) {
    +    return undefined;
    +  }
    +  const normalized = query.trim().toLowerCase();
    +  if (!normalized) {
    +    return undefined;
    +  }
    +  const exact = matches.filter((match) => {
    +    const id = match.id.trim().toLowerCase();
    +    const name = match.name?.trim().toLowerCase();
    +    const handle = match.handle?.trim().toLowerCase();
    +    return normalized === id || normalized === name || normalized === handle;
    +  });
    +  if (exact.length === 1) {
    +    return exact[0];
    +  }
    +  return undefined;
    +}
    +
    +function describeUserMatchFailure(matches: ChannelDirectoryEntry[], query: string): string {
    +  if (matches.length === 0) {
    +    return "no matches";
    +  }
    +  const normalized = query.trim().toLowerCase();
    +  if (!normalized) {
    +    return "empty input";
    +  }
    +  const exact = matches.filter((match) => {
    +    const id = match.id.trim().toLowerCase();
    +    const name = match.name?.trim().toLowerCase();
    +    const handle = match.handle?.trim().toLowerCase();
    +    return normalized === id || normalized === name || normalized === handle;
    +  });
    +  if (exact.length === 0) {
    +    return "no exact match; use full Matrix ID";
    +  }
    +  if (exact.length > 1) {
    +    return "multiple exact matches; use full Matrix ID";
    +  }
    +  return "no exact match; use full Matrix ID";
    +}
    +
     export async function resolveMatrixTargets(params: {
       cfg: unknown;
       inputs: string[];
    @@ -52,13 +98,13 @@ export async function resolveMatrixTargets(params: {
               query: trimmed,
               limit: 5,
             });
    -        const best = matches[0];
    +        const best = pickBestUserMatch(matches, trimmed);
             results.push({
               input,
               resolved: Boolean(best?.id),
               id: best?.id,
               name: best?.name,
    -          note: matches.length > 1 ? "multiple matches; chose first" : undefined,
    +          note: best ? undefined : describeUserMatchFailure(matches, trimmed),
             });
           } catch (err) {
             params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
    
  • extensions/matrix/src/types.ts+5 5 modified
    @@ -7,7 +7,7 @@ export type MatrixDmConfig = {
       enabled?: boolean;
       /** Direct message access policy (default: pairing). */
       policy?: DmPolicy;
    -  /** Allowlist for DM senders (matrix user IDs, localparts, or "*"). */
    +  /** Allowlist for DM senders (matrix user IDs or "*"). */
       allowFrom?: Array<string | number>;
     };
     
    @@ -22,7 +22,7 @@ export type MatrixRoomConfig = {
       tools?: { allow?: string[]; deny?: string[] };
       /** If true, reply without mention requirements. */
       autoReply?: boolean;
    -  /** Optional allowlist for room senders (user IDs or localparts). */
    +  /** Optional allowlist for room senders (matrix user IDs). */
       users?: Array<string | number>;
       /** Optional skill filter for this room. */
       skills?: string[];
    @@ -61,7 +61,7 @@ export type MatrixConfig = {
       allowlistOnly?: boolean;
       /** Group message policy (default: allowlist). */
       groupPolicy?: GroupPolicy;
    -  /** Allowlist for group senders (user IDs or localparts). */
    +  /** Allowlist for group senders (matrix user IDs). */
       groupAllowFrom?: Array<string | number>;
       /** Control reply threading when reply tags are present (off|first|all). */
       replyToMode?: ReplyToMode;
    @@ -79,9 +79,9 @@ export type MatrixConfig = {
       autoJoinAllowlist?: Array<string | number>;
       /** Direct message policy + allowlist overrides. */
       dm?: MatrixDmConfig;
    -  /** Room config allowlist keyed by room ID, alias, or name. */
    +  /** Room config allowlist keyed by room ID or alias (names resolved to IDs when possible). */
       groups?: Record<string, MatrixRoomConfig>;
    -  /** Room config allowlist keyed by room ID, alias, or name. Legacy; use groups. */
    +  /** Room config allowlist keyed by room ID or alias. Legacy; use groups. */
       rooms?: Record<string, MatrixRoomConfig>;
       /** Per-action tool gating (default: true for all). */
       actions?: MatrixActionConfig;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.