Skip to content

Commit 7aca070

Browse files
committed
fix(scripts): cap gh-read json bodies
1 parent e5845dd commit 7aca070

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

scripts/gh-read.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from "node:child_process";
22
import { createPrivateKey, createSign } from "node:crypto";
33
import { readFileSync } from "node:fs";
44
import { pathToFileURL } from "node:url";
5+
import { readBoundedResponseText } from "./lib/bounded-response.ts";
56
import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts";
67

78
const APP_ID_ENV = "OPENCLAW_GH_READ_APP_ID";
@@ -11,6 +12,7 @@ const PERMISSIONS_ENV = "OPENCLAW_GH_READ_PERMISSIONS";
1112
const API_VERSION = "2022-11-28";
1213
const DEFAULT_GITHUB_FETCH_TIMEOUT_MS = 30_000;
1314
const GITHUB_ERROR_BODY_MAX_CHARS = 4096;
15+
const GITHUB_JSON_BODY_MAX_BYTES = 1024 * 1024;
1416
const DEFAULT_READ_PERMISSION_KEYS = [
1517
"actions",
1618
"checks",
@@ -230,6 +232,19 @@ export async function readBoundedGitHubErrorText(
230232
return truncated ? `${text}\n[truncated]` : text;
231233
}
232234

235+
export async function readBoundedGitHubJson<T>(
236+
response: Response,
237+
maxBytes = GITHUB_JSON_BODY_MAX_BYTES,
238+
): Promise<T> {
239+
const text = await readBoundedResponseText(response, "GitHub API", maxBytes, {
240+
createTooLargeError: (message) =>
241+
Object.assign(new Error(message), {
242+
code: "ETOOBIG",
243+
}),
244+
});
245+
return JSON.parse(text) as T;
246+
}
247+
233248
export async function githubJson<T>(
234249
path: string,
235250
bearerToken: string,
@@ -263,7 +278,7 @@ export async function githubJson<T>(
263278
fail(`${init?.method ?? "GET"} ${path} failed (${response.status}): ${text}`);
264279
}
265280

266-
return (await response.json()) as T;
281+
return await readBoundedGitHubJson<T>(response);
267282
},
268283
);
269284
}

test/scripts/gh-read.test.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
parsePermissionKeys,
77
parseRepoArg,
88
readBoundedGitHubErrorText,
9+
readBoundedGitHubJson,
910
resolveGitHubFetchTimeoutMs,
1011
} from "../../scripts/gh-read.js";
1112

@@ -68,11 +69,7 @@ describe("gh-read helpers", () => {
6869
});
6970

7071
it("times out stalled GitHub API response body reads", async () => {
71-
const response = {
72-
ok: true,
73-
status: 200,
74-
json: () => new Promise(() => {}),
75-
} as Response;
72+
const response = new Response(new ReadableStream({}), { status: 200 });
7673
const request = githubJson("/app/installations", "token", undefined, {
7774
timeoutMs: 5,
7875
fetchImpl: (() => Promise.resolve(response)) as typeof fetch,
@@ -94,6 +91,53 @@ describe("gh-read helpers", () => {
9491
expect(text.length).toBeLessThan(4200);
9592
});
9693

94+
it("reads bounded GitHub API JSON responses", async () => {
95+
await expect(readBoundedGitHubJson(new Response('{"id":123}'), 1024)).resolves.toEqual({
96+
id: 123,
97+
});
98+
});
99+
100+
it("rejects oversized GitHub API JSON responses by content length", async () => {
101+
let canceled = false;
102+
const response = new Response(
103+
new ReadableStream({
104+
cancel() {
105+
canceled = true;
106+
},
107+
}),
108+
{
109+
headers: {
110+
"content-length": "1025",
111+
},
112+
},
113+
);
114+
115+
await expect(readBoundedGitHubJson(response, 1024)).rejects.toMatchObject({
116+
code: "ETOOBIG",
117+
message: "GitHub API response body exceeded 1024 bytes",
118+
});
119+
expect(canceled).toBe(true);
120+
});
121+
122+
it("rejects oversized streamed GitHub API JSON responses", async () => {
123+
const encoder = new TextEncoder();
124+
const response = new Response(
125+
new ReadableStream({
126+
start(controller) {
127+
controller.enqueue(encoder.encode('{"body":"'));
128+
controller.enqueue(encoder.encode("x".repeat(1024)));
129+
controller.enqueue(encoder.encode('"}'));
130+
controller.close();
131+
},
132+
}),
133+
);
134+
135+
await expect(readBoundedGitHubJson(response, 1024)).rejects.toMatchObject({
136+
code: "ETOOBIG",
137+
message: "GitHub API response body exceeded 1024 bytes",
138+
});
139+
});
140+
97141
it("rejects invalid GitHub API timeout values", () => {
98142
expect(resolveGitHubFetchTimeoutMs("1000")).toBe(1000);
99143
expect(() => resolveGitHubFetchTimeoutMs("1s")).toThrow(

0 commit comments

Comments
 (0)