Skip to content

Commit eb81ab8

Browse files
committed
fix(infra): cap ClawHub install-resolution JSON via shared bounded reader
The install-resolution path (fetchClawHubSkillInstallResolution) still read ClawHub JSON with an unbounded response.json(), the one ClawHub JSON reader left uncapped by the prior hardening. Route it through the existing parseClawHubJsonBody helper so every ClawHub JSON success/structured-block body is bounded by the same 16 MiB cap and cancels the stream on overflow. Pure reuse of the helper introduced in this PR (no new abstraction); adds a regression test that an oversized install-resolution body is rejected and the underlying stream is cancelled.
1 parent ef92b7f commit eb81ab8

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

src/infra/clawhub.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,42 @@ describe("clawhub helpers", () => {
854854
expect(message.length).toBeLessThanOrEqual(500);
855855
});
856856

857+
it("bounds oversized ClawHub install-resolution JSON responses and cancels the stream", async () => {
858+
const cancel = vi.fn();
859+
const chunk = new Uint8Array(512 * 1024).fill("x".charCodeAt(0));
860+
const overshootChunks = 34; // 34 * 512 KiB = 17 MiB > 16 MiB cap
861+
let emitted = 0;
862+
const body = new ReadableStream<Uint8Array>({
863+
pull(controller) {
864+
if (emitted >= overshootChunks) {
865+
controller.close();
866+
return;
867+
}
868+
emitted += 1;
869+
controller.enqueue(chunk);
870+
},
871+
cancel() {
872+
cancel();
873+
},
874+
});
875+
876+
await expect(
877+
fetchClawHubSkillInstallResolution({
878+
slug: "weather",
879+
fetchImpl: async () =>
880+
new Response(body, {
881+
status: 200,
882+
headers: { "content-type": "application/json" },
883+
}),
884+
}),
885+
).rejects.toThrow(
886+
/ClawHub \/api\/v1\/skills\/weather\/install response exceeded 16777216 bytes/,
887+
);
888+
// Same bounded reader covers the sibling install-resolution JSON path so a
889+
// hostile install response cannot exhaust memory either.
890+
expect(cancel).toHaveBeenCalledTimes(1);
891+
});
892+
857893
it("annotates 429 errors with the reset hint but no sign-in hint when authenticated", async () => {
858894
process.env.OPENCLAW_CLAWHUB_TOKEN = "env-token-123";
859895
await expect(

src/infra/clawhub.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,11 +1018,7 @@ export async function fetchClawHubSkillInstallResolution(params: {
10181018
if (!response.ok && !isStructuredBlock) {
10191019
throw await buildClawHubError(response, url, hasToken);
10201020
}
1021-
try {
1022-
return (await response.json()) as ClawHubSkillInstallResolutionResponse;
1023-
} catch (cause) {
1024-
throw new Error(`ClawHub ${url.pathname} returned malformed JSON`, { cause });
1025-
}
1021+
return parseClawHubJsonBody<ClawHubSkillInstallResolutionResponse>(response, url);
10261022
}
10271023

10281024
export async function fetchClawHubSkillVerification(params: {

0 commit comments

Comments
 (0)