From c7d3779f4f09583fc572e8d4e8af7df1e0ba049b Mon Sep 17 00:00:00 2001 From: NIO Date: Sun, 28 Jun 2026 17:04:22 +0800 Subject: [PATCH] Fix/zalo bound api json response reads (#97277) * fix(zalo): bound Bot API JSON response reads via readProviderJsonResponse * test(zalo): keep API proof in focused coverage --------- Co-authored-by: NIO Co-authored-by: Peter Steinberger (cherry picked from commit 38ddcef78f072d647136a1e28bf7383b0e203a37) --- extensions/zalo/src/api.test.ts | 50 +++++++++++++++++++++++++++++---- extensions/zalo/src/api.ts | 3 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/extensions/zalo/src/api.test.ts b/extensions/zalo/src/api.test.ts index a4df1c4cc757..3bb7fb3dceba 100644 --- a/extensions/zalo/src/api.test.ts +++ b/extensions/zalo/src/api.test.ts @@ -11,7 +11,36 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ resolvePinnedHostnameWithPolicyMock(...args), })); -import { deleteWebhook, getWebhookInfo, sendChatAction, sendPhoto, type ZaloFetch } from "./api.js"; +import { + deleteWebhook, + getMe, + getWebhookInfo, + sendChatAction, + sendPhoto, + type ZaloFetch, +} from "./api.js"; + +const ZALO_JSON_CAP_BYTES = 16 * 1024 * 1024; + +function oversizedZaloJsonResponse(onCancel: () => void): Response { + const response = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(ZALO_JSON_CAP_BYTES + 1)); + }, + cancel() { + onCancel(); + }, + }), + { headers: { "content-type": "application/json" }, status: 200 }, + ); + Object.defineProperty(response, "json", { + value: async () => { + throw new Error("unbounded json reader was used"); + }, + }); + return response; +} function createOkFetcher() { return vi.fn(async () => new Response(JSON.stringify({ ok: true, result: {} }))); @@ -103,10 +132,7 @@ describe("Zalo API request methods", () => { .mockImplementation(() => undefined); try { const fetcher = vi.fn( - async () => - ({ - json: async () => ({ ok: true, result: {} }), - }) as Response, + async () => new Response(JSON.stringify({ ok: true, result: {} })), ); await sendChatAction( @@ -199,4 +225,18 @@ describe("Zalo API request methods", () => { expect(resolvePinnedHostnameWithPolicyMock).not.toHaveBeenCalled(); expect(fetcher).not.toHaveBeenCalled(); }); + + it("bounds oversized getMe JSON responses and cancels the stream", async () => { + let cancelCount = 0; + const fetcher = vi.fn(async () => + oversizedZaloJsonResponse(() => { + cancelCount += 1; + }), + ); + + await expect(getMe("test-token", undefined, fetcher)).rejects.toThrow( + "zalo.getMe: JSON response exceeds 16777216 bytes", + ); + expect(cancelCount).toBe(1); + }); }); diff --git a/extensions/zalo/src/api.ts b/extensions/zalo/src/api.ts index ee5d83c2a1d4..9cff448dfe49 100644 --- a/extensions/zalo/src/api.ts +++ b/extensions/zalo/src/api.ts @@ -4,6 +4,7 @@ */ import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; +import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http"; import { resolvePinnedHostnameWithPolicy, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; const ZALO_API_BASE = "https://bot-api.zaloplatforms.com"; @@ -131,7 +132,7 @@ export async function callZaloApi( signal: controller.signal, }); - const data = (await response.json()) as ZaloApiResponse; + const data = await readProviderJsonResponse>(response, `zalo.${method}`); if (!data.ok) { throw new ZaloApiError(