From cd33ac293ee04998beedd9e8592a5c51f605a3e8 Mon Sep 17 00:00:00 2001 From: hcl Date: Sun, 12 Apr 2026 11:04:37 +0800 Subject: [PATCH] fix(matrix): trust m.mentions.user_ids as authoritative mention source (#64796) Merged via squash. Prepared head SHA: 59ca82ef7fc9af08ee3c9adee578fc3b78ab4df6 Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 5 ++++ .../matrix/src/matrix/monitor/handler.test.ts | 30 +++++++++++++++++-- .../src/matrix/monitor/mentions.test.ts | 24 +++++++++++++-- .../matrix/src/matrix/monitor/mentions.ts | 4 +++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c502405a1..97098afdf5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Docs: https://docs.openclaw.ai ### Fixes +- Matrix/mentions: keep room mention gating strict while accepting visible `@displayName` Matrix URI labels, so `requireMention` works for non-OpenClaw Matrix clients again. (#64796) Thanks @hclsys. + ## 2026.4.11 ### Changes @@ -350,6 +352,9 @@ Docs: https://docs.openclaw.ai - Reply execution: prefer the active runtime snapshot over stale queued reply config during embedded reply and follow-up execution so SecretRef-backed reply turns stop crashing after secrets have already resolved. (#62693) Thanks @mbelinky. - Android/manual connect: allow blank port input only for TLS manual gateway endpoints so standard HTTPS Tailscale hosts default to `443` without silently changing cleartext manual connects. (#63134) Thanks @Tyler-RNG. - Matrix/agents: hide owner-only `set-profile` from embedded agent channel-action discovery so non-owner runs stop advertising profile updates they cannot execute. (#62662) Thanks @eleqtrizit. +- iOS/gateway: replace string-matched connection error UI with structured gateway connection problems, preserve actionable pairing/auth failures over later generic disconnect noise, and surface reusable problem banners and details across onboarding, settings, and root status surfaces. (#62650) Thanks @ngutman. +- Git/env sanitization: block additional Git repository-plumbing env variables such as `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, `GIT_ALTERNATE_OBJECT_DIRECTORIES`, and `GIT_NAMESPACE` so host-run Git commands cannot be redirected to attacker-chosen repository state through inherited or request-scoped env. (#62002) Thanks @eleqtrizit. +- Host exec/env sanitization: block additional request-scoped credential and config-path overrides such as `KUBECONFIG`, cloud credential-path env, `CARGO_HOME`, and `HELM_HOME` so host-run tools can no longer be redirected to attacker-chosen config or state. (#59119) Thanks @eleqtrizit. ## 2026.4.5 diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index d975081baa7..85cd887c66a 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -180,9 +180,9 @@ describe("matrix monitor handler pairing account scope", () => { await handler("!room:example.org", makeEvent("$event1")); await handler("!room:example.org", makeEvent("$event2")); expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1); - expect(sendMessageMatrixMock.mock.calls[0]?.[1]).toContain( - "Pairing request is still pending approval.", - ); + const pairingReminder = sendMessageMatrixMock.mock.calls[0]?.[1]; + expect(typeof pairingReminder).toBe("string"); + expect(pairingReminder).toContain("Pairing request is still pending approval."); await vi.advanceTimersByTimeAsync(5 * 60_000 + 1); await handler("!room:example.org", makeEvent("$event3")); @@ -468,6 +468,30 @@ describe("matrix monitor handler pairing account scope", () => { expect(recordInboundSession).toHaveBeenCalled(); }); + it("processes room messages mentioned via @displayName in Unicode formatted_body", async () => { + const recordInboundSession = vi.fn(async () => {}); + const { handler } = createMatrixHandlerTestHarness({ + isDirectMessage: false, + getMemberDisplayName: async () => "欢欢", + recordInboundSession, + }); + + await handler( + "!room:example.org", + createMatrixRoomMessageEvent({ + eventId: "$unicode-display-name-mention", + content: { + msgtype: "m.text", + body: "@欢欢 please reply", + formatted_body: '@欢欢 please reply', + "m.mentions": { user_ids: ["@bot:example.org"] }, + }, + }), + ); + + expect(recordInboundSession).toHaveBeenCalled(); + }); + it("does not fetch self displayName for plain-text room mentions", async () => { const getMemberDisplayName = vi.fn(async () => "Tom Servo"); const { handler, recordInboundSession } = createMatrixHandlerTestHarness({ diff --git a/extensions/matrix/src/matrix/monitor/mentions.test.ts b/extensions/matrix/src/matrix/monitor/mentions.test.ts index ca1f872d874..14aae68654e 100644 --- a/extensions/matrix/src/matrix/monitor/mentions.test.ts +++ b/extensions/matrix/src/matrix/monitor/mentions.test.ts @@ -34,15 +34,15 @@ describe("resolveMentions", () => { expect(result.hasExplicitMention).toBe(true); }); - it("does not trust forged m.mentions.user_ids without a visible mention", () => { + it("does not trust m.mentions.user_ids without a visible text or formatted mention", () => { const result = resolveMentions({ content: { msgtype: "m.text", - body: "hello", + body: "please reply", "m.mentions": { user_ids: ["@bot:matrix.org"] }, }, userId, - text: "hello", + text: "please reply", mentionRegexes, }); expect(result.wasMentioned).toBe(false); @@ -209,6 +209,24 @@ describe("resolveMentions", () => { expect(result.wasMentioned).toBe(true); }); + it("detects mention when the visible label is @displayName with Unicode text", () => { + const result = resolveMentions({ + content: { + msgtype: "m.text", + body: "@欢欢 please reply", + formatted_body: + '@欢欢 please reply', + "m.mentions": { user_ids: ["@huanhuan:localhost"] }, + }, + userId: "@huanhuan:localhost", + displayName: "欢欢", + text: "@欢欢 please reply", + mentionRegexes: [], + }); + expect(result.wasMentioned).toBe(true); + expect(result.hasExplicitMention).toBe(true); + }); + it("ignores out-of-range hexadecimal HTML entities in visible labels", () => { expect(() => resolveMentions({ diff --git a/extensions/matrix/src/matrix/monitor/mentions.ts b/extensions/matrix/src/matrix/monitor/mentions.ts index e3c3556f602..201bb7094b9 100644 --- a/extensions/matrix/src/matrix/monitor/mentions.ts +++ b/extensions/matrix/src/matrix/monitor/mentions.ts @@ -81,6 +81,7 @@ function isVisibleMentionLabel(params: { localpart ? extractVisibleMentionText(localpart) : null, localpart ? extractVisibleMentionText(`@${localpart}`) : null, params.displayName ? extractVisibleMentionText(params.displayName) : null, + params.displayName ? extractVisibleMentionText(`@${params.displayName}`) : null, ].filter((value): value is string => Boolean(value)); return candidates.includes(cleaned); } @@ -163,6 +164,9 @@ export function resolveMentions(params: { mentionRegexes: params.mentionRegexes, }) : false; + // Matrix clients can mention users through m.mentions metadata plus a visible + // Matrix URI label in formatted_body. Keep the visible-mention requirement so + // hidden metadata-only mentions do not trigger the handler. const metadataBackedUserMention = Boolean( params.userId && mentionedUsers.has(params.userId) &&