From 6018174d8e0317ecd73f553ae68eae5ca1e080ba Mon Sep 17 00:00:00 2001 From: pick-cat Date: Sat, 18 Jul 2026 13:05:30 +0800 Subject: [PATCH] fix(models): reject oversized piped auth input (#109800) readPipedStdin accumulated piped stdin without a size limit, letting `models auth paste-token` and `paste-api-key` buffer arbitrarily large input before any validation. Add a 1 MiB cap matching the existing Matrix recovery-key and config-patch stdin limits, and reject oversized input at the stream boundary. --- src/commands/models/auth.test.ts | 15 +++++++++++++++ src/commands/models/auth.ts | 15 +++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/commands/models/auth.test.ts b/src/commands/models/auth.test.ts index 7ee02a1411fe..c8c6f5e34084 100644 --- a/src/commands/models/auth.test.ts +++ b/src/commands/models/auth.test.ts @@ -1473,6 +1473,21 @@ describe("modelsAuthLoginCommand", () => { expect(mocks.updateConfig).not.toHaveBeenCalled(); }); + it("rejects oversized piped auth input before buffering it", async () => { + const runtime = createRuntime(); + restoreStdin?.(); + const oversized = "x".repeat(1024 * 1024 + 1); + restoreStdin = withPipedStdin(oversized); + + await expect(modelsAuthPasteApiKeyCommand({ provider: "openai" }, runtime)).rejects.toThrow( + "Piped auth input exceeds 1048576 bytes.", + ); + + expect(mocks.clackPassword).not.toHaveBeenCalled(); + expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled(); + expect(mocks.updateConfig).not.toHaveBeenCalled(); + }); + it("writes pasted API keys to the requested agent store", async () => { const runtime = createRuntime(); useCoderAgentConfig(); diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 298aa2ae313e..5e66f8d61b96 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -130,13 +130,20 @@ const password = async (params: Parameters[0]) => const select = async (params: Parameters>[0]) => guardCancel(await clackSelect(styleSelectParams(params))); +const MODELS_AUTH_STDIN_MAX_BYTES = 1024 * 1024; + async function readPipedStdin(): Promise { - process.stdin.setEncoding("utf8"); - let input = ""; + const chunks: Buffer[] = []; + let totalBytes = 0; for await (const chunk of process.stdin) { - input += String(chunk); + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)); + totalBytes += buffer.byteLength; + if (totalBytes > MODELS_AUTH_STDIN_MAX_BYTES) { + throw new Error(`Piped auth input exceeds ${MODELS_AUTH_STDIN_MAX_BYTES} bytes.`); + } + chunks.push(buffer); } - return input; + return Buffer.concat(chunks, totalBytes).toString("utf8"); } async function readPastedSecret(params: {