Medium severity5.3NVD Advisory· Published Apr 10, 2026· Updated Apr 13, 2026
CVE-2026-35647
CVE-2026-35647
Description
OpenClaw before 2026.3.25 contains an access control vulnerability where verification notices bypass DM policy checks and reply to unpaired peers. Attackers can send verification notices to users outside allowed direct message policies by exploiting insufficient access validation before message transmission.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | <= 2026.3.24 | — |
Affected products
1Patches
12383daf5c4a4Matrix: gate verification notices on DM access (#55122)
4 files changed · +252 −0
extensions/matrix/src/matrix/monitor/events.test.ts+161 −0 modified@@ -24,6 +24,10 @@ function createHarness(params?: { cryptoAvailable?: boolean; selfUserId?: string; selfUserIdError?: Error; + allowFrom?: string[]; + dmEnabled?: boolean; + dmPolicy?: "open" | "pairing" | "allowlist" | "disabled"; + storeAllowFrom?: string[]; joinedMembersByRoom?: Record<string, string[]>; verifications?: Array<{ id: string; @@ -67,6 +71,7 @@ function createHarness(params?: { const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; const formatNativeDependencyHint = vi.fn(() => "install hint"); const logVerboseMessage = vi.fn(); + const readStoreAllowFrom = vi.fn(async () => params?.storeAllowFrom ?? []); const client = { on: vi.fn((eventName: string, listener: (...args: unknown[]) => void) => { listeners.set(eventName, listener); @@ -101,6 +106,10 @@ function createHarness(params?: { accountId: params?.accountId ?? "default", encryption: params?.authEncryption ?? true, } as MatrixAuth, + allowFrom: params?.allowFrom ?? [], + dmEnabled: params?.dmEnabled ?? true, + dmPolicy: params?.dmPolicy ?? "open", + readStoreAllowFrom, directTracker: { invalidateRoom, }, @@ -123,6 +132,7 @@ function createHarness(params?: { invalidateRoom, roomEventListener, listVerifications, + readStoreAllowFrom, logger, formatNativeDependencyHint, logVerboseMessage, @@ -255,6 +265,112 @@ describe("registerMatrixMonitorEvents verification routing", () => { expect(body).toContain('Open "Verify by emoji"'); }); + it("blocks verification request notices when dmPolicy pairing would block the sender", async () => { + const { onRoomMessage, sendMessage, roomMessageListener, logVerboseMessage } = createHarness({ + dmPolicy: "pairing", + }); + if (!roomMessageListener) { + throw new Error("room.message listener was not registered"); + } + + roomMessageListener("!room:example.org", { + event_id: "$req-pairing-blocked", + sender: "@alice:example.org", + type: EventType.RoomMessage, + origin_server_ts: Date.now(), + content: { + msgtype: "m.key.verification.request", + body: "verification request", + }, + }); + + await vi.waitFor(() => { + expect(logVerboseMessage).toHaveBeenCalledWith( + expect.stringContaining("blocked verification sender @alice:example.org"), + ); + }); + expect(sendMessage).not.toHaveBeenCalled(); + expect(onRoomMessage).not.toHaveBeenCalled(); + }); + + it("allows verification notices for pairing-authorized DM senders from the allow store", async () => { + const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({ + dmPolicy: "pairing", + storeAllowFrom: ["@alice:example.org"], + }); + if (!roomMessageListener) { + throw new Error("room.message listener was not registered"); + } + + roomMessageListener("!room:example.org", { + event_id: "$req-pairing-allowed", + sender: "@alice:example.org", + type: EventType.RoomMessage, + origin_server_ts: Date.now(), + content: { + msgtype: "m.key.verification.request", + body: "verification request", + }, + }); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenCalledTimes(1); + }); + expect(readStoreAllowFrom).toHaveBeenCalled(); + }); + + it("does not consult the allow store when dmPolicy is open", async () => { + const { sendMessage, roomMessageListener, readStoreAllowFrom } = createHarness({ + dmPolicy: "open", + }); + if (!roomMessageListener) { + throw new Error("room.message listener was not registered"); + } + + roomMessageListener("!room:example.org", { + event_id: "$req-open-policy", + sender: "@alice:example.org", + type: EventType.RoomMessage, + origin_server_ts: Date.now(), + content: { + msgtype: "m.key.verification.request", + body: "verification request", + }, + }); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenCalledTimes(1); + }); + expect(readStoreAllowFrom).not.toHaveBeenCalled(); + }); + + it("blocks verification notices when Matrix DMs are disabled", async () => { + const { sendMessage, roomMessageListener, logVerboseMessage } = createHarness({ + dmEnabled: false, + }); + if (!roomMessageListener) { + throw new Error("room.message listener was not registered"); + } + + roomMessageListener("!room:example.org", { + event_id: "$req-dm-disabled", + sender: "@alice:example.org", + type: EventType.RoomMessage, + origin_server_ts: Date.now(), + content: { + msgtype: "m.key.verification.request", + body: "verification request", + }, + }); + + await vi.waitFor(() => { + expect(logVerboseMessage).toHaveBeenCalledWith( + expect.stringContaining("blocked verification sender @alice:example.org"), + ); + }); + expect(sendMessage).not.toHaveBeenCalled(); + }); + it("posts ready-stage guidance for emoji verification", async () => { const { sendMessage, roomEventListener } = createHarness(); roomEventListener("!room:example.org", { @@ -423,6 +539,51 @@ describe("registerMatrixMonitorEvents verification routing", () => { expect(body).toContain("SAS decimal: 6158 1986 3513"); }); + it("blocks summary SAS notices when dmPolicy allowlist would block the sender", async () => { + const { sendMessage, verificationSummaryListener, logVerboseMessage } = createHarness({ + dmPolicy: "allowlist", + joinedMembersByRoom: { + "!dm:example.org": ["@alice:example.org", "@bot:example.org"], + }, + }); + if (!verificationSummaryListener) { + throw new Error("verification.summary listener was not registered"); + } + + verificationSummaryListener({ + id: "verification-blocked-summary", + roomId: "!dm:example.org", + otherUserId: "@alice:example.org", + isSelfVerification: false, + initiatedByMe: false, + phase: 3, + phaseName: "started", + pending: true, + methods: ["m.sas.v1"], + canAccept: false, + hasSas: true, + sas: { + decimal: [6158, 1986, 3513], + emoji: [ + ["🎁", "Gift"], + ["🌍", "Globe"], + ["🐴", "Horse"], + ], + }, + hasReciprocateQr: false, + completed: false, + createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(), + updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(), + }); + + await vi.waitFor(() => { + expect(logVerboseMessage).toHaveBeenCalledWith( + expect.stringContaining("blocked verification sender @alice:example.org"), + ); + }); + expect(sendMessage).not.toHaveBeenCalled(); + }); + it("posts SAS notices from summary updates using the room mapped by earlier flow events", async () => { const { sendMessage, roomEventListener, verificationSummaryListener } = createHarness({ joinedMembersByRoom: {
extensions/matrix/src/matrix/monitor/events.ts+12 −0 modified@@ -34,6 +34,10 @@ export function registerMatrixMonitorEvents(params: { cfg: CoreConfig; client: MatrixClient; auth: MatrixAuth; + allowFrom: string[]; + dmEnabled: boolean; + dmPolicy: "open" | "pairing" | "allowlist" | "disabled"; + readStoreAllowFrom: () => Promise<string[]>; directTracker?: { invalidateRoom: (roomId: string) => void; }; @@ -48,6 +52,10 @@ export function registerMatrixMonitorEvents(params: { cfg, client, auth, + allowFrom, + dmEnabled, + dmPolicy, + readStoreAllowFrom, directTracker, logVerboseMessage, warnedEncryptedRooms, @@ -58,6 +66,10 @@ export function registerMatrixMonitorEvents(params: { } = params; const { routeVerificationEvent, routeVerificationSummary } = createMatrixVerificationEventRouter({ client, + allowFrom, + dmEnabled, + dmPolicy, + readStoreAllowFrom, logVerboseMessage, });
extensions/matrix/src/matrix/monitor/index.ts+11 −0 modified@@ -266,6 +266,17 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi cfg, client, auth, + allowFrom, + dmEnabled, + dmPolicy, + readStoreAllowFrom: async () => + await core.channel.pairing + .readAllowFromStore({ + channel: "matrix", + env: process.env, + accountId: account.accountId, + }) + .catch(() => []), directTracker, logVerboseMessage, warnedEncryptedRooms,
extensions/matrix/src/matrix/monitor/verification-events.ts+68 −0 modified@@ -1,6 +1,7 @@ import { inspectMatrixDirectRooms } from "../direct-management.js"; import { isStrictDirectRoom } from "../direct-room.js"; import type { MatrixClient } from "../sdk.js"; +import { resolveMatrixMonitorAccessState } from "./access-state.js"; import type { MatrixRawEvent } from "./types.js"; import { EventType } from "./types.js"; import { @@ -309,8 +310,51 @@ async function sendVerificationNotice(params: { } } +async function isVerificationNoticeAuthorized(params: { + senderId: string; + allowFrom: string[]; + dmEnabled: boolean; + dmPolicy: "open" | "pairing" | "allowlist" | "disabled"; + readStoreAllowFrom: () => Promise<string[]>; + logVerboseMessage: (message: string) => void; +}): Promise<boolean> { + // Verification notices are DM-only. If DM ingress is disabled, there is no + // policy-compatible path for posting these notices back into the room. + if (!params.dmEnabled || params.dmPolicy === "disabled") { + params.logVerboseMessage( + `matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy}, dmEnabled=${String(params.dmEnabled)})`, + ); + return false; + } + if (params.dmPolicy === "open") { + return true; + } + const storeAllowFrom = await params.readStoreAllowFrom(); + const accessState = resolveMatrixMonitorAccessState({ + allowFrom: params.allowFrom, + storeAllowFrom, + // Verification flows only exist in strict DMs, so room/group allowlists do + // not participate in the authorization decision here. + groupAllowFrom: [], + roomUsers: [], + senderId: params.senderId, + isRoom: false, + }); + if (accessState.directAllowMatch.allowed) { + return true; + } + params.logVerboseMessage( + `matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy})`, + ); + return false; +} + export function createMatrixVerificationEventRouter(params: { client: MatrixClient; + allowFrom: string[]; + dmEnabled: boolean; + dmPolicy: "open" | "pairing" | "allowlist" | "disabled"; + readStoreAllowFrom: () => Promise<string[]>; logVerboseMessage: (message: string) => void; }) { const routerStartedAtMs = Date.now(); @@ -411,6 +455,18 @@ export function createMatrixVerificationEventRouter(params: { ); return; } + if ( + !(await isVerificationNoticeAuthorized({ + senderId: summary.otherUserId, + allowFrom: params.allowFrom, + dmEnabled: params.dmEnabled, + dmPolicy: params.dmPolicy, + readStoreAllowFrom: params.readStoreAllowFrom, + logVerboseMessage: params.logVerboseMessage, + })) + ) { + return; + } const sasNotice = formatVerificationSasNotice(summary); if (!sasNotice) { return; @@ -459,6 +515,18 @@ export function createMatrixVerificationEventRouter(params: { ); return; } + if ( + !(await isVerificationNoticeAuthorized({ + senderId, + allowFrom: params.allowFrom, + dmEnabled: params.dmEnabled, + dmPolicy: params.dmPolicy, + readStoreAllowFrom: params.readStoreAllowFrom, + logVerboseMessage: params.logVerboseMessage, + })) + ) { + return; + } rememberVerificationUserRoom(senderId, roomId); if (!trackBounded(routedVerificationEvents, sourceFingerprint)) { return;
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
4- github.com/openclaw/openclaw/commit/2383daf5c4a4e08d9553e0e949552ad755ef9ec2nvdPatchWEB
- github.com/advisories/GHSA-9wqx-g2cw-vc7rghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-9wqx-g2cw-vc7rnvdVendor AdvisoryWEB
- www.vulncheck.com/advisories/openclaw-direct-message-policy-bypass-via-verification-noticesnvdThird Party Advisory
News mentions
0No linked articles in our index yet.