From 801df108f0d18fba6893775ec9996be47efb3ba7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 7 Jun 2026 04:08:13 +0200 Subject: [PATCH] fix(cli): bound exec approvals stdin --- src/cli/exec-approvals-cli.test.ts | 10 +++++++++- src/cli/exec-approvals-cli.ts | 22 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/cli/exec-approvals-cli.test.ts b/src/cli/exec-approvals-cli.test.ts index 1b35bf5a4e5..57e4fb2a122 100644 --- a/src/cli/exec-approvals-cli.test.ts +++ b/src/cli/exec-approvals-cli.test.ts @@ -1,9 +1,10 @@ // Exec approvals CLI tests cover approval command registration and output handling. +import { Readable } from "node:stream"; import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import * as execApprovals from "../infra/exec-approvals.js"; import type { ExecApprovalsFile } from "../infra/exec-approvals.js"; -import { registerExecApprovalsCli } from "./exec-approvals-cli.js"; +import { registerExecApprovalsCli, testing } from "./exec-approvals-cli.js"; const mocks = vi.hoisted(() => { const runtimeErrors: string[] = []; @@ -560,4 +561,11 @@ describe("exec approvals CLI", () => { }); expect(runtimeErrors).toHaveLength(0); }); + + it("bounds approvals JSON read from stdin", async () => { + await expect(testing.readStdin(Readable.from(["12345"]), 5)).resolves.toBe("12345"); + await expect(testing.readStdin(Readable.from(["12345", "6"]), 5)).rejects.toThrow( + "Exec approvals stdin exceeds 5 bytes.", + ); + }); }); diff --git a/src/cli/exec-approvals-cli.ts b/src/cli/exec-approvals-cli.ts index 6212e7303f7..1cf8583579c 100644 --- a/src/cli/exec-approvals-cli.ts +++ b/src/cli/exec-approvals-cli.ts @@ -46,6 +46,7 @@ type EffectivePolicyReport = { note?: string; }; const APPROVALS_GET_DEFAULT_TIMEOUT_MS = 60_000; +const EXEC_APPROVALS_STDIN_MAX_BYTES = 1024 * 1024; type ExecApprovalsCliOpts = NodesRpcOpts & { node?: string; @@ -55,12 +56,21 @@ type ExecApprovalsCliOpts = NodesRpcOpts & { agent?: string; }; -async function readStdin(): Promise { +async function readStdin( + stream: NodeJS.ReadableStream = process.stdin, + maxBytes = EXEC_APPROVALS_STDIN_MAX_BYTES, +): Promise { const chunks: Buffer[] = []; - for await (const chunk of process.stdin) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); + let total = 0; + for await (const chunk of stream) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); + total += buffer.byteLength; + if (total > maxBytes) { + throw new Error(`Exec approvals stdin exceeds ${maxBytes} bytes.`); + } + chunks.push(buffer); } - return Buffer.concat(chunks).toString("utf8"); + return Buffer.concat(chunks, total).toString("utf8"); } async function resolveTargetNodeId(opts: ExecApprovalsCliOpts): Promise { @@ -622,3 +632,7 @@ export function registerExecApprovalsCli(program: Command) { applyParentDefaultHelpAction(approvals); } + +export const testing = { + readStdin, +};