diff --git a/packages/coding-agent/src/core/tools/computer.ts b/packages/coding-agent/src/core/tools/computer.ts index 48c425f..f8ea171 100644 --- a/packages/coding-agent/src/core/tools/computer.ts +++ b/packages/coding-agent/src/core/tools/computer.ts @@ -31,6 +31,7 @@ const computerActions = [ ] as const; const computerObservationModes = ["hybrid", "ocr"] as const; +const computerSnapshotIdPattern = /^[A-Za-z0-9_-]+$/; const DEFAULT_COMPUTER_COMMAND = 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 { const targetCount = (input.ref !== undefined ? 1 : 0) + @@ -307,6 +314,10 @@ function validateWaitInput(input: ComputerToolInput): void { } function validateComputerInput(input: ComputerToolInput): void { + if (input.snapshotId !== undefined) { + validateSnapshotId(input.snapshotId); + } + switch (input.action) { case "observe": case "app_list": diff --git a/packages/coding-agent/test/computer-tool.test.ts b/packages/coding-agent/test/computer-tool.test.ts index 5194d6b..bf254c2 100644 --- a/packages/coding-agent/test/computer-tool.test.ts +++ b/packages/coding-agent/test/computer-tool.test.ts @@ -159,6 +159,27 @@ describe("computer tool", () => { 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", () => { const parsed = parseArgs(["--tools", "computer,read"]); expect(parsed.tools).toEqual(["computer", "read"]); @@ -234,4 +255,29 @@ describe("computer tool", () => { expect(result.stderr).toContain("app_not_found:"); 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"); + }); });