fix(scripts): bound GHSA patch GitHub lookups (#110756)

* fix(scripts): bound GHSA patch subprocesses

Co-authored-by: 唐梓夷0668001293 <tang.ziyi@xydigit.com>

* fix(scripts): satisfy GHSA runner control-flow lint

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
tzy-17
2026-07-19 00:37:06 +01:00
committed by GitHub
co-authored by Peter Steinberger
parent ed458f14dc
commit 828bcd8231
4 changed files with 110 additions and 16 deletions
+25 -16
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env node
// Applies GHSA patch payloads to advisory branches.
import { execFileSync, spawnSync } from "node:child_process";
import { execFileSync } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { GHSA_COMMAND_TIMEOUT_MS, runGhCommand } from "./lib/ghsa-patch-subprocess.mjs";
function usage() {
console.error(
@@ -44,15 +45,19 @@ function parseArgs(argv) {
}
function runGh(args) {
const proc = spawnSync("gh", args, { encoding: "utf8" });
if (proc.status !== 0) {
fail(proc.stderr.trim() || proc.stdout.trim() || `gh ${args.join(" ")} failed`);
try {
return runGhCommand(args);
} catch (error) {
return fail(error instanceof Error ? error.message : String(error));
}
return proc.stdout;
}
function deriveRepoFromOrigin() {
const remote = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8" }).trim();
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
encoding: "utf8",
killSignal: "SIGKILL",
timeout: GHSA_COMMAND_TIMEOUT_MS,
}).trim();
const httpsMatch = remote.match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?$/);
if (!httpsMatch) {
fail(`Could not parse origin remote: ${remote}`);
@@ -125,16 +130,20 @@ const payload = {
};
const patchFile = writeTempJson(payload);
runGh([
"api",
"-H",
"X-GitHub-Api-Version: 2022-11-28",
"-X",
"PATCH",
advisoryPath,
"--input",
patchFile,
]);
try {
runGh([
"api",
"-H",
"X-GitHub-Api-Version: 2022-11-28",
"-X",
"PATCH",
advisoryPath,
"--input",
patchFile,
]);
} finally {
fs.rmSync(patchFile, { force: true });
}
if (restoredCvss) {
runGh([
+22
View File
@@ -0,0 +1,22 @@
export const GHSA_COMMAND_TIMEOUT_MS: number;
export function runGhCommand(
args: string[],
params?: {
spawnSyncImpl?: (
command: string,
args: string[],
options: {
encoding: "utf8";
killSignal: "SIGKILL";
timeout: number;
},
) => {
error?: Error;
status: number | null;
stderr: string;
stdout: string;
};
timeoutMs?: number;
},
): string;
+22
View File
@@ -0,0 +1,22 @@
import { spawnSync } from "node:child_process";
// GHSA patch performs multiple sequential GitHub API reads and writes. Keep enough
// headroom for GitHub latency while preventing one stalled request from blocking
// the maintainer command indefinitely.
export const GHSA_COMMAND_TIMEOUT_MS = 60_000;
export function runGhCommand(args, params = {}) {
const spawnSyncImpl = params.spawnSyncImpl ?? spawnSync;
const proc = spawnSyncImpl("gh", args, {
encoding: "utf8",
killSignal: "SIGKILL",
timeout: params.timeoutMs ?? GHSA_COMMAND_TIMEOUT_MS,
});
if (proc.error) {
throw proc.error;
}
if (proc.status !== 0) {
throw new Error(proc.stderr.trim() || proc.stdout.trim() || `gh ${args.join(" ")} failed`);
}
return proc.stdout;
}
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from "vitest";
import { runGhCommand } from "../../scripts/lib/ghsa-patch-subprocess.mjs";
describe("GHSA patch subprocess", () => {
it("bounds each GitHub lookup with a timeout and SIGKILL", () => {
const spawnSyncImpl = vi.fn(() => ({
status: 0,
stdout: "result",
stderr: "",
}));
expect(runGhCommand(["api", "rate_limit"], { spawnSyncImpl })).toBe("result");
expect(spawnSyncImpl).toHaveBeenCalledWith("gh", ["api", "rate_limit"], {
encoding: "utf8",
killSignal: "SIGKILL",
timeout: 60_000,
});
});
it("throws the GitHub CLI error when a lookup exits non-zero", () => {
const spawnSyncImpl = vi.fn(() => ({
status: 1,
stdout: "",
stderr: "gh api failed",
}));
expect(() => runGhCommand(["api", "rate_limit"], { spawnSyncImpl })).toThrow("gh api failed");
});
it("propagates the timeout error from a stalled GitHub CLI process", () => {
const timeout = Object.assign(new Error("spawnSync gh ETIMEDOUT"), { code: "ETIMEDOUT" });
const spawnSyncImpl = vi.fn(() => ({
error: timeout,
status: null,
stdout: "",
stderr: "",
}));
expect(() => runGhCommand(["api", "rate_limit"], { spawnSyncImpl })).toThrow(timeout);
});
});