diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 76b7be4b72b..d978049b925 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -277,8 +277,6 @@ export namespace File { const project = Instance.project const full = path.join(Instance.directory, file) - // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. - // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. if (!Instance.containsPath(full)) { throw new Error(`Access denied: path escapes project directory`) } @@ -337,8 +335,6 @@ export namespace File { } const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory - // TODO: Filesystem.contains is lexical only - symlinks inside the project can escape. - // TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization. if (!Instance.containsPath(resolved)) { throw new Error(`Access denied: path escapes project directory`) } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 472bff83dd3..f58367c135b 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -22,8 +22,31 @@ export namespace Filesystem { return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..") } - export function contains(parent: string, child: string) { - return !relative(parent, child).startsWith("..") + /** + * Check if a path is within a parent directory, with protection against: + * - Symlink escapes: Resolves symlinks to their real paths + * - Cross-drive paths on Windows: Checks drive letters match + * Returns true if child is within parent, false otherwise. + */ + export function contains(parent: string, child: string): boolean { + try { + const realParent = realpathSync(parent) + const realChild = realpathSync(child) + + if (process.platform === "win32") { + const parentDrive = realParent.split(":")[0]?.toLowerCase() + const childDrive = realChild.split(":")[0]?.toLowerCase() + if (parentDrive !== childDrive) { + return false + } + } + + const rel = relative(realParent, realChild) + return !rel.startsWith("..") && rel !== ".." + } catch { + const rel = relative(parent, child) + return !rel.startsWith("..") + } } export async function findUp(target: string, start: string, stop?: string) {