Medium severity6.5NVD Advisory· Published Mar 31, 2026· Updated Apr 1, 2026
CVE-2026-33576
CVE-2026-33576
Description
OpenClaw before 2026.3.28 downloads and stores inbound media from Zalo channels before validating sender authorization. Unauthorized senders can force network fetches and disk writes to the media store by sending messages that are subsequently rejected.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.28 | 2026.3.28 |
Affected products
1Patches
168ceaf7a5f64zalo: gate image downloads before DM auth (#55979)
2 files changed · +99 −15
extensions/zalo/src/monitor.image.polling.test.ts+35 −0 modified@@ -8,6 +8,7 @@ import { getUpdatesMock, getZaloRuntimeMock, resetLifecycleTestState, + sendMessageMock, } from "../../../test/helpers/extensions/zalo-lifecycle.js"; describe("Zalo polling image handling", () => { @@ -62,4 +63,38 @@ describe("Zalo polling image handling", () => { abort.abort(); await run; }); + + it("rejects unauthorized DM images before downloading media", async () => { + getUpdatesMock + .mockResolvedValueOnce({ + ok: true, + result: createImageUpdate(), + }) + .mockImplementation(() => new Promise(() => {})); + + const { monitorZaloProvider } = await import("./monitor.js"); + const abort = new AbortController(); + const runtime = createRuntimeEnv(); + const { account, config } = createLifecycleMonitorSetup({ + accountId: "default", + dmPolicy: "pairing", + allowFrom: ["allowed-user"], + }); + const run = monitorZaloProvider({ + token: "zalo-token", // pragma: allowlist secret + account, + config, + runtime, + abortSignal: abort.signal, + }); + + await vi.waitFor(() => expect(sendMessageMock).toHaveBeenCalledTimes(1)); + expect(fetchRemoteMediaMock).not.toHaveBeenCalled(); + expect(saveMediaBufferMock).not.toHaveBeenCalled(); + expect(finalizeInboundContextMock).not.toHaveBeenCalled(); + expect(recordInboundSessionMock).not.toHaveBeenCalled(); + + abort.abort(); + await run; + }); });
extensions/zalo/src/monitor.ts+64 −15 modified@@ -93,11 +93,20 @@ type ZaloMessagePipelineParams = ZaloProcessingContext & { text?: string; mediaPath?: string; mediaType?: string; + authorization?: ZaloMessageAuthorizationResult; }; type ZaloImageMessageParams = ZaloProcessingContext & { message: ZaloMessage; mediaMaxMb: number; }; +type ZaloMessageAuthorizationResult = { + chatId: string; + commandAuthorized: boolean | undefined; + isGroup: boolean; + rawBody: string; + senderId: string; + senderName: string | undefined; +}; function formatZaloError(error: unknown): string { if (error instanceof Error) { @@ -285,6 +294,16 @@ async function handleTextMessage( async function handleImageMessage(params: ZaloImageMessageParams): Promise<void> { const { message, mediaMaxMb, account, core, runtime } = params; const { photo_url, caption } = message; + const authorization = await authorizeZaloMessage({ + ...params, + text: caption, + // Use a sentinel so auth sees this as an inbound image before the download happens. + mediaPath: photo_url ? "__pending_media__" : undefined, + mediaType: undefined, + }); + if (!authorization) { + return; + } let mediaPath: string | undefined; let mediaType: string | undefined; @@ -308,32 +327,24 @@ async function handleImageMessage(params: ZaloImageMessageParams): Promise<void> await processMessageWithPipeline({ ...params, + authorization, text: caption, mediaPath, mediaType, }); } -async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Promise<void> { - const { - message, - token, - account, - config, - runtime, - core, - text, - mediaPath, - mediaType, - statusSink, - fetcher, - } = params; +async function authorizeZaloMessage( + params: ZaloMessagePipelineParams, +): Promise<ZaloMessageAuthorizationResult | undefined> { + const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } = + params; const pairing = createChannelPairingController({ core, channel: "zalo", accountId: account.accountId, }); - const { from, chat, message_id, date } = message; + const { from, chat } = message; const isGroup = chat.chat_type === "GROUP"; const chatId = chat.id; @@ -436,6 +447,44 @@ async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Pr return; } + return { + chatId, + commandAuthorized, + isGroup, + rawBody, + senderId, + senderName, + }; +} + +async function processMessageWithPipeline(params: ZaloMessagePipelineParams): Promise<void> { + const { + message, + token, + account, + config, + runtime, + core, + text, + mediaPath, + mediaType, + statusSink, + fetcher, + authorization: authorizationOverride, + } = params; + const { message_id, date } = message; + const authorization = + authorizationOverride ?? + (await authorizeZaloMessage({ + ...params, + mediaPath, + mediaType, + })); + if (!authorization) { + return; + } + const { isGroup, chatId, senderId, senderName, rawBody, commandAuthorized } = authorization; + const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({ cfg: config, channel: "zalo",
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/openclaw/openclaw/commit/68ceaf7a5f64a23e78b95eff055e4b497218312anvdPatchWEB
- github.com/advisories/GHSA-v2v2-f783-358jghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-v2v2-f783-358jnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-33576ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-unauthorized-media-download-via-zalo-channelnvdThird Party AdvisoryWEB
News mentions
0No linked articles in our index yet.