fix: path traversal security vulnerability in screenshot/pdf/eval commands

- Add validateOutputPath() to ensure screenshot, pdf, and responsive
  commands only write to /tmp or current working directory
- Add validateReadPath() to prevent eval command from reading
  files outside allowed directories or using path traversal (..)
- Validates both absolute paths and relative paths with .. sequences

Security impact: Prevents arbitrary file write (screenshot/pdf) and
arbitrary file read (eval) attacks via path traversal.

Fixes: #13
This commit is contained in:
Jah-yee 2026-03-13 08:19:23 +08:00
parent 1b317aae9a
commit 39702a90ae
2 changed files with 65 additions and 0 deletions

View File

@ -6,6 +6,43 @@ import type { BrowserManager } from './browser-manager';
import { handleSnapshot } from './snapshot';
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()];
/**
* Validates that a file path is within allowed directories
* @param filePath The path to validate
* @throws Error if path is outside safe directories
*/
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(', ')}`);
}
}
/**
* Validates that a file path for reading doesn't contain path traversal
* @param filePath The path to validate
* @throws Error if path contains traversal sequences
*/
function validateReadPath(filePath: string): void {
// Reject absolute paths outside allowed directories
if (path.isAbsolute(filePath)) {
const isSafe = SAFE_DIRECTORIES.some(dir => path.resolve(filePath).startsWith(dir));
if (!isSafe) {
throw new Error(`Absolute path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
}
}
// Check for path traversal sequences
const normalized = path.normalize(filePath);
if (normalized.includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
}
}
export async function handleMetaCommand(
command: string,
@ -73,6 +110,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}`;
}
@ -80,6 +118,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}`;
}
@ -87,6 +126,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 },

View File

@ -8,6 +8,30 @@
import type { BrowserManager } from './browser-manager';
import { consoleBuffer, networkBuffer } from './buffers';
import * as fs from 'fs';
import * as path from 'path';
// Security: Path validation to prevent path traversal attacks
const SAFE_DIRECTORIES = ['/tmp', process.cwd()];
/**
* Validates that a file path for reading doesn't contain path traversal
* @param filePath The path to validate
* @throws Error if path contains traversal sequences or is outside safe directories
*/
function validateReadPath(filePath: string): void {
// Reject absolute paths outside allowed directories
if (path.isAbsolute(filePath)) {
const isSafe = SAFE_DIRECTORIES.some(dir => path.resolve(filePath).startsWith(dir));
if (!isSafe) {
throw new Error(`Absolute path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
}
}
// Check for path traversal sequences
const normalized = path.normalize(filePath);
if (normalized.includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
}
}
export async function handleReadCommand(
command: string,
@ -98,6 +122,7 @@ export async function handleReadCommand(
case 'eval': {
const filePath = args[0];
if (!filePath) throw new Error('Usage: browse eval <js-file>');
validateReadPath(filePath);
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
const code = fs.readFileSync(filePath, 'utf-8');
const result = await page.evaluate(code);