From fe6880bff0897c5380c8e5d6f6c66c5a7f85ff83 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 5 Apr 2026 11:45:32 -0700 Subject: [PATCH] fix(browse): symlink resolution in meta-commands validateOutputPath (#820) Add realpathSync to validateOutputPath in meta-commands.ts to catch symlink-based directory escapes in screenshot, pdf, and responsive commands. Resolves SAFE_DIRECTORIES through realpathSync to handle macOS /tmp -> /private/tmp symlinks. Existing path validation tests pass with the hardened implementation. Co-authored-by: garagon Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/meta-commands.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index e2060c21..ac5655aa 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -15,11 +15,30 @@ import { resolveConfig } from './config'; import type { Frame } from 'playwright'; // Security: Path validation to prevent path traversal attacks -const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()]; +// Resolve safe directories through realpathSync to handle symlinks (e.g., macOS /tmp → /private/tmp) +const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()].map(d => { + try { return fs.realpathSync(d); } catch { return d; } +}); export function validateOutputPath(filePath: string): void { const resolved = path.resolve(filePath); - const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir)); + + // Resolve real path of the parent directory to catch symlinks. + // The file itself may not exist yet (e.g., screenshot output). + let dir = path.dirname(resolved); + let realDir: string; + try { + realDir = fs.realpathSync(dir); + } catch { + try { + realDir = fs.realpathSync(path.dirname(dir)); + } catch { + throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); + } + } + + const realResolved = path.join(realDir, path.basename(resolved)); + const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realResolved, dir)); if (!isSafe) { throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); }