fix: validate computer snapshot ids

- reject unsafe snapshot ids in the TypeScript wrapper before spawning the helper
- reject unsafe snapshot ids in agent-computer before loading snapshot files
- add regression coverage for wrapper and helper traversal attempts

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Harivansh Rathi 2026-03-11 14:34:10 -04:00
parent a4250bad30
commit 8a43732b7e
2 changed files with 57 additions and 0 deletions

View file

@ -31,6 +31,7 @@ const computerActions = [
] as const; ] as const;
const computerObservationModes = ["hybrid", "ocr"] as const; const computerObservationModes = ["hybrid", "ocr"] as const;
const computerSnapshotIdPattern = /^[A-Za-z0-9_-]+$/;
const DEFAULT_COMPUTER_COMMAND = const DEFAULT_COMPUTER_COMMAND =
process.env.COMPANION_AGENT_COMPUTER_COMMAND || "agent-computer"; process.env.COMPANION_AGENT_COMPUTER_COMMAND || "agent-computer";
@ -285,6 +286,12 @@ function hasDragDestination(input: ComputerToolInput): boolean {
); );
} }
function validateSnapshotId(snapshotId: string): void {
if (!computerSnapshotIdPattern.test(snapshotId)) {
throw new Error(`Invalid computer snapshotId: "${snapshotId}"`);
}
}
function validateWaitInput(input: ComputerToolInput): void { function validateWaitInput(input: ComputerToolInput): void {
const targetCount = const targetCount =
(input.ref !== undefined ? 1 : 0) + (input.ref !== undefined ? 1 : 0) +
@ -307,6 +314,10 @@ function validateWaitInput(input: ComputerToolInput): void {
} }
function validateComputerInput(input: ComputerToolInput): void { function validateComputerInput(input: ComputerToolInput): void {
if (input.snapshotId !== undefined) {
validateSnapshotId(input.snapshotId);
}
switch (input.action) { switch (input.action) {
case "observe": case "observe":
case "app_list": case "app_list":

View file

@ -159,6 +159,27 @@ describe("computer tool", () => {
expect(calls).toHaveLength(0); expect(calls).toHaveLength(0);
}); });
it("rejects unsafe snapshot ids before spawning the helper", async () => {
const cwd = createTempDir("coding-agent-computer-snapshot-id-");
const stateDir = join(cwd, "computer-state");
const { calls, operations } = createMockComputerOperations();
const computerTool = createComputerTool(cwd, {
operations,
stateDir,
});
await expect(
computerTool.execute("computer-click-invalid-snapshot", {
action: "click",
snapshotId: "../../auth",
ref: "w1",
}),
).rejects.toThrow('Invalid computer snapshotId: "../../auth"');
expect(calls).toHaveLength(0);
});
it("accepts computer in --tools and exposes it in built-in tool wiring", () => { it("accepts computer in --tools and exposes it in built-in tool wiring", () => {
const parsed = parseArgs(["--tools", "computer,read"]); const parsed = parseArgs(["--tools", "computer,read"]);
expect(parsed.tools).toEqual(["computer", "read"]); expect(parsed.tools).toEqual(["computer", "read"]);
@ -234,4 +255,29 @@ describe("computer tool", () => {
expect(result.stderr).toContain("app_not_found:"); expect(result.stderr).toContain("app_not_found:");
expect(existsSync(markerPath)).toBe(false); expect(existsSync(markerPath)).toBe(false);
}); });
it("rejects snapshot path traversal inside the helper", () => {
const stateDir = createTempDir("coding-agent-computer-helper-snapshot-id-");
const result = spawnSync(
process.execPath,
[
"--no-warnings",
getAgentComputerScriptPath(),
"--state-dir",
stateDir,
"--input",
JSON.stringify({
action: "click",
snapshotId: "../../auth",
ref: "w1",
}),
],
{
encoding: "utf8",
},
);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("invalid_snapshot_id: ../../auth");
});
}); });