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 <nocodet@mail.com>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
(cherry picked from commit 38ddcef78f)
This commit is contained in:
NIO
2026-06-28 17:04:22 +08:00
committed by Dallin Romney
parent c43269ccf9
commit c7d3779f4f
2 changed files with 47 additions and 6 deletions
+45 -5
View File
@@ -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<Uint8Array>({
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<ZaloFetch>(async () => new Response(JSON.stringify({ ok: true, result: {} })));
@@ -103,10 +132,7 @@ describe("Zalo API request methods", () => {
.mockImplementation(() => undefined);
try {
const fetcher = vi.fn<ZaloFetch>(
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<ZaloFetch>(async () =>
oversizedZaloJsonResponse(() => {
cancelCount += 1;
}),
);
await expect(getMe("test-token", undefined, fetcher)).rejects.toThrow(
"zalo.getMe: JSON response exceeds 16777216 bytes",
);
expect(cancelCount).toBe(1);
});
});
+2 -1
View File
@@ -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<T = unknown>(
signal: controller.signal,
});
const data = (await response.json()) as ZaloApiResponse<T>;
const data = await readProviderJsonResponse<ZaloApiResponse<T>>(response, `zalo.${method}`);
if (!data.ok) {
throw new ZaloApiError(