Skip to content

Commit 7d937f0

Browse files
committed
fix(security): suppress unhandled stdout/stderr stream errors in install policy
1 parent f04d18a commit 7d937f0

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

src/security/install-policy.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
// Covers install-policy checks for packages and plugin installs.
2+
import type { ChildProcess } from "node:child_process";
3+
import { EventEmitter } from "node:events";
24
import fs from "node:fs/promises";
35
import os from "node:os";
46
import path from "node:path";
57
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8+
9+
const spawnMock = vi.hoisted(() => vi.fn());
10+
vi.mock("node:child_process", async (importOriginal) => {
11+
const actual = await importOriginal<typeof import("node:child_process")>();
12+
return {
13+
...actual,
14+
spawn: (...args: Parameters<typeof actual.spawn>) =>
15+
spawnMock(...args) ?? actual.spawn(...args),
16+
};
17+
});
618
import type { OpenClawConfig } from "../config/types.openclaw.js";
719
import {
820
killPidIfAlive,
@@ -674,3 +686,75 @@ describe("runInstallPolicy", () => {
674686
},
675687
);
676688
});
689+
690+
describe("runPolicyCommand stream error handling", () => {
691+
function createFakeChild(): ChildProcess {
692+
const child = new EventEmitter() as EventEmitter & ChildProcess;
693+
child.stdout = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stdout"]>;
694+
child.stderr = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stderr"]>;
695+
child.stdin = new EventEmitter() as EventEmitter & NonNullable<ChildProcess["stdin"]>;
696+
child.stdin.write = vi.fn(() => true) as NonNullable<ChildProcess["stdin"]>["write"];
697+
child.stdin.end = vi.fn() as NonNullable<ChildProcess["stdin"]>["end"];
698+
Object.defineProperties(child, {
699+
pid: { configurable: true, enumerable: true, get: () => 1234 },
700+
killed: { configurable: true, enumerable: true, get: () => false },
701+
});
702+
child.kill = vi.fn(() => true) as ChildProcess["kill"];
703+
return child;
704+
}
705+
706+
beforeEach(() => {
707+
spawnMock.mockReset();
708+
});
709+
710+
it("swallows stdout and stderr stream errors without rejecting", { timeout: 5_000 }, async () => {
711+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-policy-stream-"));
712+
tempDirs.push(dir);
713+
const policyScriptPath = path.join(dir, "policy.cjs");
714+
await fs.writeFile(policyScriptPath, "module.exports = {};", "utf8");
715+
await fs.chmod(policyScriptPath, 0o700);
716+
717+
spawnMock.mockImplementation(() => {
718+
const child = createFakeChild();
719+
const response = Buffer.from(JSON.stringify({ protocolVersion: 1, decision: "allow" }));
720+
// Emit stream errors and response after listeners are attached in runPolicyCommand.
721+
queueMicrotask(() => {
722+
child.stdout?.emit("error", new Error("stdout read failed"));
723+
child.stdout?.emit("data", response);
724+
child.stderr?.emit("error", new Error("stderr read failed"));
725+
child.emit("close", 0, null);
726+
});
727+
return child;
728+
});
729+
730+
await expect(
731+
runInstallPolicy({
732+
config: {
733+
security: {
734+
installPolicy: {
735+
enabled: true,
736+
exec: {
737+
source: "exec",
738+
command: policyScriptPath,
739+
args: [],
740+
allowInsecurePath: true,
741+
timeoutMs: 5_000,
742+
noOutputTimeoutMs: 5_000,
743+
maxOutputBytes: 16 * 1024,
744+
},
745+
},
746+
},
747+
},
748+
request: {
749+
targetType: "skill",
750+
targetName: "weather",
751+
sourcePath: dir,
752+
sourcePathKind: "directory",
753+
source: { kind: "clawhub", authority: "openclaw", mutable: false, network: true },
754+
origin: { type: "clawhub" },
755+
request: { kind: "skill-install", mode: "install" },
756+
},
757+
}),
758+
).resolves.toEqual({});
759+
});
760+
});

src/security/install-policy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,7 +600,9 @@ async function runPolicyCommand(params: {
600600
clearTimers();
601601
reject(error);
602602
});
603+
child.stdout?.on("error", () => {});
603604
child.stdout?.on("data", (chunk) => append(chunk, "stdout"));
605+
child.stderr?.on("error", () => {});
604606
child.stderr?.on("data", (chunk) => append(chunk, "stderr"));
605607
child.on("close", (code, signal) => {
606608
if (settled) {

0 commit comments

Comments
 (0)