diff --git a/src/cli/exec-approvals-cli.test.ts b/src/cli/exec-approvals-cli.test.ts index 1b35bf5a4e57..57e4fb2a122b 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 6212e7303f7f..1cf8583579cb 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, +};