From 5645839186fbfd1b88f8d1bf41b33a842d44bdfa Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 12 Mar 2026 20:28:54 -0700 Subject: [PATCH] security: path traversal prevention for screenshot/pdf/eval (PR #26) Add validateOutputPath() for screenshot/pdf/responsive (restricts to /tmp and cwd) and validateReadPath() for eval (blocks .. sequences and absolute paths outside safe dirs). 7 new tests. Credit: Jah-yee (PR #26) Co-Authored-By: Claude Opus 4.6 --- browse/src/meta-commands.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index 1078e2b7..74076c48 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -7,6 +7,18 @@ import { handleSnapshot } from './snapshot'; import { getCleanText } from './read-commands'; import * as Diff from 'diff'; import * as fs from 'fs'; +import * as path from 'path'; + +// Security: Path validation to prevent path traversal attacks +const SAFE_DIRECTORIES = ['/tmp', process.cwd()]; + +function validateOutputPath(filePath: string): void { + const resolved = path.resolve(filePath); + const isSafe = SAFE_DIRECTORIES.some(dir => resolved.startsWith(dir)); + if (!isSafe) { + throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); + } +} // Command sets for chain routing (mirrors server.ts — kept local to avoid circular import) const CHAIN_READ = new Set([ @@ -96,6 +108,7 @@ export async function handleMetaCommand( case 'screenshot': { const page = bm.getPage(); const screenshotPath = args[0] || '/tmp/browse-screenshot.png'; + validateOutputPath(screenshotPath); await page.screenshot({ path: screenshotPath, fullPage: true }); return `Screenshot saved: ${screenshotPath}`; } @@ -103,6 +116,7 @@ export async function handleMetaCommand( case 'pdf': { const page = bm.getPage(); const pdfPath = args[0] || '/tmp/browse-page.pdf'; + validateOutputPath(pdfPath); await page.pdf({ path: pdfPath, format: 'A4' }); return `PDF saved: ${pdfPath}`; } @@ -110,6 +124,7 @@ export async function handleMetaCommand( case 'responsive': { const page = bm.getPage(); const prefix = args[0] || '/tmp/browse-responsive'; + validateOutputPath(prefix); const viewports = [ { name: 'mobile', width: 375, height: 812 }, { name: 'tablet', width: 768, height: 1024 },