Skip to content

Commit 36bfe77

Browse files
committed
fix(github): bound guard response bodies
1 parent def4c99 commit 36bfe77

4 files changed

Lines changed: 97 additions & 6 deletions

File tree

scripts/github/dependency-guard.mjs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const dependencyGraphGuardMarker = "<!-- openclaw:dependency-graph-guard
1111
export const dependencyChangedLabel = "dependencies-changed";
1212
export const allowDependenciesCommand = "/allow-dependencies-change";
1313
export const GITHUB_ERROR_BODY_MAX_BYTES = 64 * 1024;
14+
export const GITHUB_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
1415
export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
1516

1617
const maxListedFiles = 25;
@@ -489,10 +490,31 @@ function githubErrorBodyTooLarge(maxBytes) {
489490
return new Error(`GitHub error response body exceeded ${maxBytes} bytes`);
490491
}
491492

492-
export async function readBoundedGitHubErrorText(response, maxBytes = GITHUB_ERROR_BODY_MAX_BYTES) {
493+
function githubResponseBodyTooLarge(maxBytes) {
494+
return new Error(`GitHub response body exceeded ${maxBytes} bytes`);
495+
}
496+
497+
export async function readBoundedGitHubErrorText(
498+
response,
499+
maxBytes = GITHUB_ERROR_BODY_MAX_BYTES,
500+
options = {},
501+
) {
493502
return await readBoundedResponseText(response, "GitHub error", maxBytes, {
494503
createTooLargeError: () => githubErrorBodyTooLarge(maxBytes),
504+
...options,
505+
});
506+
}
507+
508+
export async function readBoundedGitHubJson(
509+
response,
510+
maxBytes = GITHUB_RESPONSE_BODY_MAX_BYTES,
511+
options = {},
512+
) {
513+
const text = await readBoundedResponseText(response, "GitHub", maxBytes, {
514+
createTooLargeError: () => githubResponseBodyTooLarge(maxBytes),
515+
...options,
495516
});
517+
return JSON.parse(text);
496518
}
497519

498520
function timeoutError(path, method, timeoutMs) {
@@ -513,6 +535,7 @@ function combineAbortSignals(signals) {
513535
export function githubApi(token, options = {}) {
514536
const fetchImpl = options.fetchImpl ?? fetch;
515537
const timeoutMs = options.timeoutMs ?? GITHUB_API_REQUEST_TIMEOUT_MS;
538+
const responseMaxBodyBytes = options.responseMaxBodyBytes ?? GITHUB_RESPONSE_BODY_MAX_BYTES;
516539
const baseHeaders = {
517540
accept: "application/vnd.github+json",
518541
authorization: `Bearer ${token}`,
@@ -542,15 +565,21 @@ export function githubApi(token, options = {}) {
542565
if (!response.ok) {
543566
let errorText;
544567
try {
545-
errorText = await readBoundedGitHubErrorText(response);
568+
errorText = await readBoundedGitHubErrorText(response, GITHUB_ERROR_BODY_MAX_BYTES, {
569+
signal: timeoutController.signal,
570+
timeoutPromise,
571+
});
546572
} catch (bodyError) {
547573
errorText = bodyError instanceof Error ? bodyError.message : String(bodyError);
548574
}
549575
const error = new Error(`${response.status} ${response.statusText}: ${errorText}`);
550576
error.status = response.status;
551577
throw error;
552578
}
553-
return response.json();
579+
return await readBoundedGitHubJson(response, responseMaxBodyBytes, {
580+
signal: timeoutController.signal,
581+
timeoutPromise,
582+
});
554583
})();
555584
operationPromise.catch(() => {});
556585
try {

scripts/github/security-sensitive-guard.mjs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const securitySensitiveGuardMarker = "<!-- openclaw:security-sensitive-gu
1010
export const securitySensitiveChangedLabel = "security-sensitive-changed";
1111
export const allowSecuritySensitiveCommand = "/allow-security-sensitive-change";
1212
export const GITHUB_ERROR_BODY_MAX_BYTES = 64 * 1024;
13+
export const GITHUB_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
1314
export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
1415

1516
const securityTeamSlug = process.env.OPENCLAW_SECURITY_TEAM_SLUG ?? "openclaw-secops";
@@ -349,10 +350,31 @@ function githubErrorBodyTooLarge(maxBytes) {
349350
return new Error(`GitHub error response body exceeded ${maxBytes} bytes`);
350351
}
351352

352-
export async function readBoundedGitHubErrorText(response, maxBytes = GITHUB_ERROR_BODY_MAX_BYTES) {
353+
function githubResponseBodyTooLarge(maxBytes) {
354+
return new Error(`GitHub response body exceeded ${maxBytes} bytes`);
355+
}
356+
357+
export async function readBoundedGitHubErrorText(
358+
response,
359+
maxBytes = GITHUB_ERROR_BODY_MAX_BYTES,
360+
options = {},
361+
) {
353362
return await readBoundedResponseText(response, "GitHub error", maxBytes, {
354363
createTooLargeError: () => githubErrorBodyTooLarge(maxBytes),
364+
...options,
365+
});
366+
}
367+
368+
export async function readBoundedGitHubJson(
369+
response,
370+
maxBytes = GITHUB_RESPONSE_BODY_MAX_BYTES,
371+
options = {},
372+
) {
373+
const text = await readBoundedResponseText(response, "GitHub", maxBytes, {
374+
createTooLargeError: () => githubResponseBodyTooLarge(maxBytes),
375+
...options,
355376
});
377+
return JSON.parse(text);
356378
}
357379

358380
function timeoutError(path, method, timeoutMs) {
@@ -373,6 +395,7 @@ function combineAbortSignals(signals) {
373395
export function githubApi(token, options = {}) {
374396
const fetchImpl = options.fetchImpl ?? fetch;
375397
const timeoutMs = options.timeoutMs ?? GITHUB_API_REQUEST_TIMEOUT_MS;
398+
const responseMaxBodyBytes = options.responseMaxBodyBytes ?? GITHUB_RESPONSE_BODY_MAX_BYTES;
376399
const baseHeaders = {
377400
accept: "application/vnd.github+json",
378401
authorization: `Bearer ${token}`,
@@ -402,15 +425,21 @@ export function githubApi(token, options = {}) {
402425
if (!response.ok) {
403426
let errorText;
404427
try {
405-
errorText = await readBoundedGitHubErrorText(response);
428+
errorText = await readBoundedGitHubErrorText(response, GITHUB_ERROR_BODY_MAX_BYTES, {
429+
signal: timeoutController.signal,
430+
timeoutPromise,
431+
});
406432
} catch (bodyError) {
407433
errorText = bodyError instanceof Error ? bodyError.message : String(bodyError);
408434
}
409435
const error = new Error(`${response.status} ${response.statusText}: ${errorText}`);
410436
error.status = response.status;
411437
throw error;
412438
}
413-
return response.json();
439+
return await readBoundedGitHubJson(response, responseMaxBodyBytes, {
440+
signal: timeoutController.signal,
441+
timeoutPromise,
442+
});
414443
})();
415444
operationPromise.catch(() => {});
416445
try {

test/scripts/dependency-guard-script.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { afterEach, describe, expect, it, vi } from "vitest";
33
import {
44
GITHUB_ERROR_BODY_MAX_BYTES,
5+
GITHUB_RESPONSE_BODY_MAX_BYTES,
56
canAutoscrubPullRequest,
67
createAutoscrubCommit,
78
dependencyGuardCommentAuthors,
@@ -669,6 +670,21 @@ describe("dependency guard script", () => {
669670
}
670671
});
671672

673+
it("bounds successful GitHub API response bodies", async () => {
674+
const request = githubApi("token", {
675+
responseMaxBodyBytes: 64,
676+
fetchImpl: (() =>
677+
Promise.resolve(
678+
new Response("x".repeat(65), {
679+
headers: { "content-length": "65" },
680+
}),
681+
)) as typeof fetch,
682+
}).request("/repos/openclaw/openclaw");
683+
684+
await expect(request).rejects.toThrow("GitHub response body exceeded 64 bytes");
685+
expect(GITHUB_RESPONSE_BODY_MAX_BYTES).toBeGreaterThan(64);
686+
});
687+
672688
it("aborts stalled GitHub API fetches at the request timeout", async () => {
673689
let signal: AbortSignal | undefined;
674690
let markFetchStarted!: () => void;

test/scripts/security-sensitive-guard-script.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Security Sensitive Guard Script tests cover sensitive file guard behavior.
22
import { describe, expect, it } from "vitest";
33
import {
4+
GITHUB_RESPONSE_BODY_MAX_BYTES,
45
allowSecuritySensitiveCommand,
56
collectSecuritySensitiveChanges,
67
findSecuritySensitiveOverrideCommand,
78
findSecuritySensitiveOverrideCommandAsync,
89
findTrustedSecuritySensitiveGuardActor,
10+
githubApi,
911
isSecuritySensitiveFile,
1012
isSecuritySensitiveGuardAuthorizedForHead,
1113
isSecuritySensitiveGuardMarkerComment,
@@ -245,4 +247,19 @@ describe("security-sensitive guard script", () => {
245247
new Set(["vincentkoc", "steipete", "joshavant"]),
246248
);
247249
});
250+
251+
it("bounds successful GitHub API response bodies", async () => {
252+
const request = githubApi("token", {
253+
responseMaxBodyBytes: 64,
254+
fetchImpl: (() =>
255+
Promise.resolve(
256+
new Response("x".repeat(65), {
257+
headers: { "content-length": "65" },
258+
}),
259+
)) as typeof fetch,
260+
}).request("/repos/openclaw/openclaw");
261+
262+
await expect(request).rejects.toThrow("GitHub response body exceeded 64 bytes");
263+
expect(GITHUB_RESPONSE_BODY_MAX_BYTES).toBeGreaterThan(64);
264+
});
248265
});

0 commit comments

Comments
 (0)