From 387fadb2c9066885c70848bc2978f3af02d5255c Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 27 Jan 2026 12:47:33 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20block=20tilde=20paths=20f?= =?UTF-8?q?rom=20escaping=20workspace=20restrictions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tilde-prefixed paths (e.g., ~/cmux/file.ts) could bypass workspace directory restrictions because validatePathInCwd() checked the path before tilde expansion. Node's path.isAbsolute('~/...') returns false, so these paths were incorrectly treated as relative and resolved within cwd, passing validation. The actual file operation then expanded the tilde, accessing files outside the workspace. Fix: expand tildes in validatePathInCwd() before path resolution so validation sees the actual destination path. Adds tests for tilde path rejection. --- src/node/services/tools/fileCommon.test.ts | 19 +++++++++++++++++++ src/node/services/tools/fileCommon.ts | 11 +++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/node/services/tools/fileCommon.test.ts b/src/node/services/tools/fileCommon.test.ts index 2acb50334e..8caccea833 100644 --- a/src/node/services/tools/fileCommon.test.ts +++ b/src/node/services/tools/fileCommon.test.ts @@ -147,6 +147,25 @@ describe("fileCommon", () => { const result = validatePathInCwd("../outside.ts", cwdWithSlash, runtime); expect(result).not.toBeNull(); }); + + it("should reject tilde paths outside cwd", () => { + // Tilde paths expand to home directory, which is outside /workspace/project + const result = validatePathInCwd("~/other-project/file.ts", cwd, runtime); + expect(result).not.toBeNull(); + expect(result?.error).toContain("restricted to the workspace directory"); + }); + + it("should reject tilde paths to sensitive files", () => { + const result = validatePathInCwd("~/.ssh/id_rsa", cwd, runtime); + expect(result).not.toBeNull(); + expect(result?.error).toContain("restricted to the workspace directory"); + }); + + it("should reject bare tilde path", () => { + const result = validatePathInCwd("~", cwd, runtime); + expect(result).not.toBeNull(); + expect(result?.error).toContain("restricted to the workspace directory"); + }); }); describe("validateNoRedundantPrefix", () => { diff --git a/src/node/services/tools/fileCommon.ts b/src/node/services/tools/fileCommon.ts index 08e3aa7902..aa9adb0b51 100644 --- a/src/node/services/tools/fileCommon.ts +++ b/src/node/services/tools/fileCommon.ts @@ -3,6 +3,7 @@ import assert from "@/common/utils/assert"; import { createPatch } from "diff"; import type { FileStat, Runtime } from "@/node/runtime/Runtime"; import { SSHRuntime } from "@/node/runtime/SSHRuntime"; +import { expandTilde } from "@/node/runtime/tildeExpansion"; import type { ToolConfiguration } from "@/common/utils/tools/tools"; /** @@ -216,13 +217,19 @@ export function validatePathInCwd( assert(path.isAbsolute(dir), `extraAllowedDir must be an absolute path: '${dir}'`); } - const filePathIsAbsolute = path.isAbsolute(filePath); + // Expand tildes FIRST so we validate the actual destination path. + // Without this, ~/outside/file.ts would be treated as a relative path + // (path.isAbsolute('~/...') returns false) and incorrectly pass validation. + const expandedPath = expandTilde(filePath); + const filePathIsAbsolute = path.isAbsolute(expandedPath); // Only allow extraAllowedDirs when the caller provides an absolute path. // This prevents relative-path escapes (e.g., ../...) from bypassing cwd restrictions. // Resolve the path (handles relative paths and normalizes) - const resolvedPath = filePathIsAbsolute ? path.resolve(filePath) : path.resolve(cwd, filePath); + const resolvedPath = filePathIsAbsolute + ? path.resolve(expandedPath) + : path.resolve(cwd, expandedPath); const allowedRoots = [cwd, ...(filePathIsAbsolute ? trimmedExtraAllowedDirs : [])].map((dir) => path.resolve(dir)