From 33e105713a994294dfadb712aec9016059d32b1e Mon Sep 17 00:00:00 2001 From: Superagent Date: Thu, 2 Jul 2026 19:29:22 +0000 Subject: [PATCH] Apply Superagent patch: Fix: Path Traversal in File Tools --- src/tools/file.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/tools/file.ts b/src/tools/file.ts index 13b388f91..7d290fc53 100644 --- a/src/tools/file.ts +++ b/src/tools/file.ts @@ -1,6 +1,6 @@ import { createTwoFilesPatch } from "diff"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; -import { dirname, isAbsolute, resolve } from "path"; +import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs"; +import { dirname, isAbsolute, relative, resolve } from "path"; import { summarizeDiagnostics, syncFileWithLsp } from "../lsp/runtime"; import type { LspDiagnosticFile } from "../lsp/types"; @@ -19,7 +19,39 @@ export interface FileResult { lspDiagnostics?: LspDiagnosticFile[]; } +function safeRealpath(p: string): string { + try { + return realpathSync(p); + } catch { + return resolve(p); + } +} + +/** + * Ensure a user/agent-supplied path cannot escape the workspace, whether via + * lexical traversal (e.g. `../../.ssh/id_rsa`) or a symlink that crosses + * outside the workspace root. Mirrors the directory-prefix guards applied + * elsewhere in this repo (e.g. `assertInsideSchedulesDir`). Throws on escape + * so the calling tool surfaces a clean failure instead of touching the host. + */ +function assertInsideWorkspace(filePath: string, cwd: string): void { + const root = safeRealpath(cwd); + const full = isAbsolute(filePath) ? filePath : resolve(cwd, filePath); + + const rel = relative(root, full); + if (rel.startsWith("..") || isAbsolute(rel)) { + throw new Error(`Path "${filePath}" resolves outside the workspace and was rejected.`); + } + + const realTarget = safeRealpath(full); + const realRel = relative(root, realTarget); + if (realRel.startsWith("..") || isAbsolute(realRel)) { + throw new Error(`Path "${filePath}" points outside the workspace via a symlink and was rejected.`); + } +} + function resolvePath(filePath: string, cwd: string): string { + assertInsideWorkspace(filePath, cwd); return isAbsolute(filePath) ? filePath : resolve(cwd, filePath); }