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 },