mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(github): bound guard response bodies
This commit is contained in:
@@ -11,6 +11,7 @@ export const dependencyGraphGuardMarker = "<!-- openclaw:dependency-graph-guard
|
||||
export const dependencyChangedLabel = "dependencies-changed";
|
||||
export const allowDependenciesCommand = "/allow-dependencies-change";
|
||||
export const GITHUB_ERROR_BODY_MAX_BYTES = 64 * 1024;
|
||||
export const GITHUB_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
|
||||
export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
const maxListedFiles = 25;
|
||||
@@ -489,12 +490,33 @@ function githubErrorBodyTooLarge(maxBytes) {
|
||||
return new Error(`GitHub error response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubErrorText(response, maxBytes = GITHUB_ERROR_BODY_MAX_BYTES) {
|
||||
function githubResponseBodyTooLarge(maxBytes) {
|
||||
return new Error(`GitHub response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubErrorText(
|
||||
response,
|
||||
maxBytes = GITHUB_ERROR_BODY_MAX_BYTES,
|
||||
options = {},
|
||||
) {
|
||||
return await readBoundedResponseText(response, "GitHub error", maxBytes, {
|
||||
createTooLargeError: () => githubErrorBodyTooLarge(maxBytes),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubJson(
|
||||
response,
|
||||
maxBytes = GITHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
options = {},
|
||||
) {
|
||||
const text = await readBoundedResponseText(response, "GitHub", maxBytes, {
|
||||
createTooLargeError: () => githubResponseBodyTooLarge(maxBytes),
|
||||
...options,
|
||||
});
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
function timeoutError(path, method, timeoutMs) {
|
||||
return new Error(`GitHub API ${method} ${path} exceeded timeout ${timeoutMs}ms`);
|
||||
}
|
||||
@@ -513,6 +535,7 @@ function combineAbortSignals(signals) {
|
||||
export function githubApi(token, options = {}) {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const timeoutMs = options.timeoutMs ?? GITHUB_API_REQUEST_TIMEOUT_MS;
|
||||
const responseMaxBodyBytes = options.responseMaxBodyBytes ?? GITHUB_RESPONSE_BODY_MAX_BYTES;
|
||||
const baseHeaders = {
|
||||
accept: "application/vnd.github+json",
|
||||
authorization: `Bearer ${token}`,
|
||||
@@ -542,7 +565,10 @@ export function githubApi(token, options = {}) {
|
||||
if (!response.ok) {
|
||||
let errorText;
|
||||
try {
|
||||
errorText = await readBoundedGitHubErrorText(response);
|
||||
errorText = await readBoundedGitHubErrorText(response, GITHUB_ERROR_BODY_MAX_BYTES, {
|
||||
signal: timeoutController.signal,
|
||||
timeoutPromise,
|
||||
});
|
||||
} catch (bodyError) {
|
||||
errorText = bodyError instanceof Error ? bodyError.message : String(bodyError);
|
||||
}
|
||||
@@ -550,7 +576,10 @@ export function githubApi(token, options = {}) {
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
return await readBoundedGitHubJson(response, responseMaxBodyBytes, {
|
||||
signal: timeoutController.signal,
|
||||
timeoutPromise,
|
||||
});
|
||||
})();
|
||||
operationPromise.catch(() => {});
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const securitySensitiveGuardMarker = "<!-- openclaw:security-sensitive-gu
|
||||
export const securitySensitiveChangedLabel = "security-sensitive-changed";
|
||||
export const allowSecuritySensitiveCommand = "/allow-security-sensitive-change";
|
||||
export const GITHUB_ERROR_BODY_MAX_BYTES = 64 * 1024;
|
||||
export const GITHUB_RESPONSE_BODY_MAX_BYTES = 4 * 1024 * 1024;
|
||||
export const GITHUB_API_REQUEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
const securityTeamSlug = process.env.OPENCLAW_SECURITY_TEAM_SLUG ?? "openclaw-secops";
|
||||
@@ -349,12 +350,33 @@ function githubErrorBodyTooLarge(maxBytes) {
|
||||
return new Error(`GitHub error response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubErrorText(response, maxBytes = GITHUB_ERROR_BODY_MAX_BYTES) {
|
||||
function githubResponseBodyTooLarge(maxBytes) {
|
||||
return new Error(`GitHub response body exceeded ${maxBytes} bytes`);
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubErrorText(
|
||||
response,
|
||||
maxBytes = GITHUB_ERROR_BODY_MAX_BYTES,
|
||||
options = {},
|
||||
) {
|
||||
return await readBoundedResponseText(response, "GitHub error", maxBytes, {
|
||||
createTooLargeError: () => githubErrorBodyTooLarge(maxBytes),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function readBoundedGitHubJson(
|
||||
response,
|
||||
maxBytes = GITHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
options = {},
|
||||
) {
|
||||
const text = await readBoundedResponseText(response, "GitHub", maxBytes, {
|
||||
createTooLargeError: () => githubResponseBodyTooLarge(maxBytes),
|
||||
...options,
|
||||
});
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
function timeoutError(path, method, timeoutMs) {
|
||||
return new Error(`GitHub API ${method} ${path} exceeded timeout ${timeoutMs}ms`);
|
||||
}
|
||||
@@ -373,6 +395,7 @@ function combineAbortSignals(signals) {
|
||||
export function githubApi(token, options = {}) {
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const timeoutMs = options.timeoutMs ?? GITHUB_API_REQUEST_TIMEOUT_MS;
|
||||
const responseMaxBodyBytes = options.responseMaxBodyBytes ?? GITHUB_RESPONSE_BODY_MAX_BYTES;
|
||||
const baseHeaders = {
|
||||
accept: "application/vnd.github+json",
|
||||
authorization: `Bearer ${token}`,
|
||||
@@ -402,7 +425,10 @@ export function githubApi(token, options = {}) {
|
||||
if (!response.ok) {
|
||||
let errorText;
|
||||
try {
|
||||
errorText = await readBoundedGitHubErrorText(response);
|
||||
errorText = await readBoundedGitHubErrorText(response, GITHUB_ERROR_BODY_MAX_BYTES, {
|
||||
signal: timeoutController.signal,
|
||||
timeoutPromise,
|
||||
});
|
||||
} catch (bodyError) {
|
||||
errorText = bodyError instanceof Error ? bodyError.message : String(bodyError);
|
||||
}
|
||||
@@ -410,7 +436,10 @@ export function githubApi(token, options = {}) {
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response.json();
|
||||
return await readBoundedGitHubJson(response, responseMaxBodyBytes, {
|
||||
signal: timeoutController.signal,
|
||||
timeoutPromise,
|
||||
});
|
||||
})();
|
||||
operationPromise.catch(() => {});
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
GITHUB_ERROR_BODY_MAX_BYTES,
|
||||
GITHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
canAutoscrubPullRequest,
|
||||
createAutoscrubCommit,
|
||||
dependencyGuardCommentAuthors,
|
||||
@@ -669,6 +670,21 @@ describe("dependency guard script", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("bounds successful GitHub API response bodies", async () => {
|
||||
const request = githubApi("token", {
|
||||
responseMaxBodyBytes: 64,
|
||||
fetchImpl: (() =>
|
||||
Promise.resolve(
|
||||
new Response("x".repeat(65), {
|
||||
headers: { "content-length": "65" },
|
||||
}),
|
||||
)) as typeof fetch,
|
||||
}).request("/repos/openclaw/openclaw");
|
||||
|
||||
await expect(request).rejects.toThrow("GitHub response body exceeded 64 bytes");
|
||||
expect(GITHUB_RESPONSE_BODY_MAX_BYTES).toBeGreaterThan(64);
|
||||
});
|
||||
|
||||
it("aborts stalled GitHub API fetches at the request timeout", async () => {
|
||||
let signal: AbortSignal | undefined;
|
||||
let markFetchStarted!: () => void;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Security Sensitive Guard Script tests cover sensitive file guard behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GITHUB_RESPONSE_BODY_MAX_BYTES,
|
||||
allowSecuritySensitiveCommand,
|
||||
collectSecuritySensitiveChanges,
|
||||
findSecuritySensitiveOverrideCommand,
|
||||
findSecuritySensitiveOverrideCommandAsync,
|
||||
findTrustedSecuritySensitiveGuardActor,
|
||||
githubApi,
|
||||
isSecuritySensitiveFile,
|
||||
isSecuritySensitiveGuardAuthorizedForHead,
|
||||
isSecuritySensitiveGuardMarkerComment,
|
||||
@@ -245,4 +247,19 @@ describe("security-sensitive guard script", () => {
|
||||
new Set(["vincentkoc", "steipete", "joshavant"]),
|
||||
);
|
||||
});
|
||||
|
||||
it("bounds successful GitHub API response bodies", async () => {
|
||||
const request = githubApi("token", {
|
||||
responseMaxBodyBytes: 64,
|
||||
fetchImpl: (() =>
|
||||
Promise.resolve(
|
||||
new Response("x".repeat(65), {
|
||||
headers: { "content-length": "65" },
|
||||
}),
|
||||
)) as typeof fetch,
|
||||
}).request("/repos/openclaw/openclaw");
|
||||
|
||||
await expect(request).rejects.toThrow("GitHub response body exceeded 64 bytes");
|
||||
expect(GITHUB_RESPONSE_BODY_MAX_BYTES).toBeGreaterThan(64);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user