diff --git a/scripts/e2e/secret-provider-integrations.mjs b/scripts/e2e/secret-provider-integrations.mjs index ed574f91469c..1fb53ff9e613 100644 --- a/scripts/e2e/secret-provider-integrations.mjs +++ b/scripts/e2e/secret-provider-integrations.mjs @@ -45,6 +45,11 @@ const OUTPUT_CAPTURE_LIMIT_BYTES = readPositiveInt( 4 * 1024 * 1024, "OPENCLAW_SECRET_PROOF_OUTPUT_BYTES", ); +const RESOLVER_STDIN_LIMIT_BYTES = readPositiveInt( + process.env.OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES, + 1024 * 1024, + "OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES", +); const RESULTS_PATH = process.env.OPENCLAW_SECRET_PROOF_RESULTS_PATH?.trim() || path.join(os.tmpdir(), `openclaw-secret-provider-e2e-results-${process.pid}.json`); @@ -596,15 +601,36 @@ if (!storePath) { console.error("missing PROOF_SECRET_STORE_PATH"); process.exit(4); } +const stdinLimitBytes = ${RESOLVER_STDIN_LIMIT_BYTES}; function readStdin() { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { let body = ""; + let bytes = 0; + let failed = false; + const fail = (error) => { + if (failed) { + return; + } + failed = true; + reject(error); + }; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { + bytes += Buffer.byteLength(chunk, "utf8"); + if (bytes > stdinLimitBytes) { + fail(new Error(\`resolver stdin exceeded \${stdinLimitBytes} bytes\`)); + process.stdin.destroy(); + return; + } body += chunk; }); - process.stdin.on("end", () => resolve(body)); + process.stdin.on("error", fail); + process.stdin.on("end", () => { + if (!failed) { + resolve(body); + } + }); }); } @@ -1835,6 +1861,7 @@ export { runCommand, startGateway, waitForManagedGatewayStatus, + writeProofPlugin, }; if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { diff --git a/test/scripts/secret-provider-integrations.test.ts b/test/scripts/secret-provider-integrations.test.ts index a7deed5495e9..73276b5fdf44 100644 --- a/test/scripts/secret-provider-integrations.test.ts +++ b/test/scripts/secret-provider-integrations.test.ts @@ -258,6 +258,7 @@ describe("secret provider integration proof harness", () => { ["OPENCLAW_SECRET_PROOF_COMMAND_MS", "150ms"], ["OPENCLAW_SECRET_PROOF_READY_MS", "0"], ["OPENCLAW_SECRET_PROOF_OUTPUT_BYTES", "4mb"], + ["OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES", "4mb"], ])("rejects malformed proof env limit %s=%s", async (name, value) => { const previous = process.env[name]; process.env[name] = value; @@ -274,6 +275,48 @@ describe("secret provider integration proof harness", () => { } }); + it("bounds generated resolver stdin before reading the secret store", async () => { + const root = makeTempDir(); + const stateDir = path.join(root, "state"); + fs.mkdirSync(stateDir, { recursive: true }); + const storePath = path.join(stateDir, "proof-secret-store.json"); + fs.writeFileSync( + storePath, + `${JSON.stringify({ mode: "ok", calls: 0, values: { "proof/id": "ok" } }, null, 2)}\n`, + "utf8", + ); + const previousLimit = process.env.OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES; + process.env.OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES = "64"; + + try { + const proof = await import( + `${pathToFileURL(proofScriptPath).href}?case=resolver-stdin-${Date.now()}` + ); + const plugin = proof.writeProofPlugin({ stateDir }); + const result = spawnSync(process.execPath, [plugin.resolverPath], { + cwd: plugin.pluginRoot, + encoding: "utf8", + env: { + ...process.env, + PROOF_SECRET_STORE_PATH: storePath, + }, + input: JSON.stringify({ ids: ["proof/id"], padding: "x".repeat(512) }), + timeout: 5_000, + }); + + expect(result.error).toBeUndefined(); + expect(result.status).not.toBe(0); + expect(`${result.stderr}${result.stdout}`).toContain("resolver stdin exceeded 64 bytes"); + expect(JSON.parse(fs.readFileSync(storePath, "utf8")).calls).toBe(0); + } finally { + if (previousLimit === undefined) { + delete process.env.OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES; + } else { + process.env.OPENCLAW_SECRET_PROOF_RESOLVER_STDIN_BYTES = previousLimit; + } + } + }); + it("fails when proof temp cleanup cannot remove the root", async () => { const proof = await import(`${pathToFileURL(proofScriptPath).href}?case=cleanup-${Date.now()}`); const rmSync = vi.spyOn(fs, "rmSync").mockImplementation(() => {