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: {