Moderate severityNVD Advisory· Published Feb 19, 2026· Updated Feb 20, 2026
OpenClaw iMessage group allowlist authorization inherited DM pairing-store identities
CVE-2026-26328
Description
OpenClaw is a personal AI assistant. Prior to version 2026.2.14, under iMessage groupPolicy=allowlist, group authorization could be satisfied by sender identities coming from the DM pairing store, broadening DM trust into group contexts. Version 2026.2.14 fixes the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.14 | 2026.2.14 |
clawdbotnpm | < 2026.2.14 | 2026.2.14 |
Affected products
2- openclaw/clawdbotv5Range: <= 2026.1.24-3
Patches
1872079d42fe1fix(imessage): keep DM pairing-store identities out of group allowlist auth
2 files changed · +231 −3
src/imessage/monitor/monitor-provider.ts+87 −3 modified@@ -111,6 +111,88 @@ function describeReplyContext(message: IMessagePayload): IMessageReplyContext | return { body, id, sender }; } +function isRecord(value: unknown): value is Record<string, unknown> { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isOptionalString(value: unknown): value is string | null | undefined { + return value === undefined || value === null || typeof value === "string"; +} + +function isOptionalStringOrNumber(value: unknown): value is string | number | null | undefined { + return ( + value === undefined || value === null || typeof value === "string" || typeof value === "number" + ); +} + +function isOptionalNumber(value: unknown): value is number | null | undefined { + return value === undefined || value === null || typeof value === "number"; +} + +function isOptionalBoolean(value: unknown): value is boolean | null | undefined { + return value === undefined || value === null || typeof value === "boolean"; +} + +function isOptionalStringArray(value: unknown): value is string[] | null | undefined { + return ( + value === undefined || + value === null || + (Array.isArray(value) && value.every((entry) => typeof entry === "string")) + ); +} + +function isOptionalAttachments(value: unknown): value is IMessagePayload["attachments"] { + if (value === undefined || value === null) { + return true; + } + if (!Array.isArray(value)) { + return false; + } + return value.every((attachment) => { + if (!isRecord(attachment)) { + return false; + } + return ( + isOptionalString(attachment.original_path) && + isOptionalString(attachment.mime_type) && + isOptionalBoolean(attachment.missing) + ); + }); +} + +function parseIMessageNotification(raw: unknown): IMessagePayload | null { + if (!isRecord(raw)) { + return null; + } + const maybeMessage = raw.message; + if (!isRecord(maybeMessage)) { + return null; + } + + const message: IMessagePayload = maybeMessage; + if ( + !isOptionalNumber(message.id) || + !isOptionalNumber(message.chat_id) || + !isOptionalString(message.sender) || + !isOptionalBoolean(message.is_from_me) || + !isOptionalString(message.text) || + !isOptionalStringOrNumber(message.reply_to_id) || + !isOptionalString(message.reply_to_text) || + !isOptionalString(message.reply_to_sender) || + !isOptionalString(message.created_at) || + !isOptionalAttachments(message.attachments) || + !isOptionalString(message.chat_identifier) || + !isOptionalString(message.chat_guid) || + !isOptionalString(message.chat_name) || + !isOptionalStringArray(message.participants) || + !isOptionalBoolean(message.is_group) + ) { + return null; + } + + return message; +} + /** * Cache for recently sent messages, used for echo detection. * Keys are scoped by conversation (accountId:target) so the same text in different chats is not conflated. @@ -294,7 +376,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const effectiveDmAllowFrom = Array.from(new Set([...allowFrom, ...storeAllowFrom])) .map((v) => String(v).trim()) .filter(Boolean); - const effectiveGroupAllowFrom = Array.from(new Set([...groupAllowFrom, ...storeAllowFrom])) + // Keep DM pairing-store authorization scoped to DMs; group access must come + // from explicit group allowlist config. + const effectiveGroupAllowFrom = Array.from(new Set(groupAllowFrom)) .map((v) => String(v).trim()) .filter(Boolean); @@ -676,9 +760,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P } const handleMessage = async (raw: unknown) => { - const params = raw as { message?: IMessagePayload | null }; - const message = params?.message ?? null; + const message = parseIMessageNotification(raw); if (!message) { + logVerbose("imessage: dropping malformed RPC message payload"); return; } await inboundDebouncer.enqueue({ message });
src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts+144 −0 modified@@ -31,6 +31,29 @@ function getConfig(): TestConfig { } describe("monitorIMessageProvider", () => { + it("ignores malformed rpc message payloads", async () => { + const run = monitorIMessageProvider(); + await waitForSubscribe(); + + notificationHandler?.({ + method: "message", + params: { + message: { + id: 1, + sender: { nested: "not-a-string" }, + text: "hello", + }, + }, + }); + + await flush(); + closeResolve?.(); + await run; + + expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); + }); + it("skips group messages without a mention by default", async () => { const run = monitorIMessageProvider(); await waitForSubscribe(); @@ -364,6 +387,127 @@ describe("monitorIMessageProvider", () => { await run; expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); + }); + + it("does not allow group sender from pairing store when groupPolicy is allowlist", async () => { + config = { + ...config, + channels: { + ...config.channels, + imessage: { + ...config.channels?.imessage, + dmPolicy: "pairing", + allowFrom: [], + groupPolicy: "allowlist", + groupAllowFrom: [], + }, + }, + }; + readAllowFromStoreMock.mockResolvedValue(["+15550003333"]); + const run = monitorIMessageProvider(); + await waitForSubscribe(); + + notificationHandler?.({ + method: "message", + params: { + message: { + id: 30, + chat_id: 909, + sender: "+15550003333", + is_from_me: false, + text: "@openclaw hi from paired sender", + is_group: true, + }, + }, + }); + + await flush(); + closeResolve?.(); + await run; + + expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); + }); + + it("does not allow sender from pairing store when groupAllowFrom is restricted to a different chat_id", async () => { + config = { + ...config, + channels: { + ...config.channels, + imessage: { + ...config.channels?.imessage, + dmPolicy: "pairing", + allowFrom: [], + groupPolicy: "allowlist", + groupAllowFrom: ["chat_id:101"], + }, + }, + }; + readAllowFromStoreMock.mockResolvedValue(["+15550003333"]); + const run = monitorIMessageProvider(); + await waitForSubscribe(); + + notificationHandler?.({ + method: "message", + params: { + message: { + id: 31, + chat_id: 202, + sender: "+15550003333", + is_from_me: false, + text: "@openclaw hi from paired sender", + is_group: true, + }, + }, + }); + + await flush(); + closeResolve?.(); + await run; + + expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); + }); + + it("does not authorize control command via pairing-store sender in non-allowlisted chat", async () => { + config = { + ...config, + channels: { + ...config.channels, + imessage: { + ...config.channels?.imessage, + dmPolicy: "pairing", + allowFrom: [], + groupPolicy: "allowlist", + groupAllowFrom: ["chat_id:101"], + }, + }, + }; + readAllowFromStoreMock.mockResolvedValue(["+15550003333"]); + const run = monitorIMessageProvider(); + await waitForSubscribe(); + + notificationHandler?.({ + method: "message", + params: { + message: { + id: 32, + chat_id: 202, + sender: "+15550003333", + is_from_me: false, + text: "/status", + is_group: true, + }, + }, + }); + + await flush(); + closeResolve?.(); + await run; + + expect(replyMock).not.toHaveBeenCalled(); + expect(sendMock).not.toHaveBeenCalled(); }); it("blocks group messages when groupPolicy is disabled", async () => {
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
5- github.com/advisories/GHSA-g34w-4xqq-h79mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-26328ghsaADVISORY
- github.com/openclaw/openclaw/commit/872079d42fe105ece2900a1dd6ab321b92da2d59ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.14ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/security/advisories/GHSA-g34w-4xqq-h79mghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.