From 15345a42ec4ea62fe5590dd838d6f61de1156695 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Fri, 13 Mar 2026 03:21:06 +0800 Subject: [PATCH 1/2] Security: Fix path traversal in screenshot/pdf/eval commands - Add validateOutputPath() to restrict output paths to /tmp or CWD - Add validateFilePath() to prevent arbitrary file read via eval - Resolves issue #13 (path traversal allows arbitrary file read/write) --- browse/src/meta-commands.ts | 19 +++++++++++++++++++ browse/src/read-commands.ts | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index 0fbe9aea4..652e074f2 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -6,6 +6,22 @@ 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'; + +/** + * Validates that a file path is within an allowed directory to prevent path traversal attacks. + * Allowed directories: /tmp and the current working directory. + */ +function validateOutputPath(filePath: string): void { + const resolvedPath = path.resolve(filePath); + const tmpDir = '/tmp'; + const cwd = process.cwd(); + + // Allow paths in /tmp or under CWD + if (!resolvedPath.startsWith(tmpDir + '/') && !resolvedPath.startsWith(cwd + '/')) { + throw new Error(`Security: Output path must be within /tmp or the current working directory. Received: ${filePath}`); + } +} export async function handleMetaCommand( command: string, @@ -73,6 +89,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 +97,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 +105,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 }, diff --git a/browse/src/read-commands.ts b/browse/src/read-commands.ts index a473477d0..741b8077c 100644 --- a/browse/src/read-commands.ts +++ b/browse/src/read-commands.ts @@ -8,6 +8,27 @@ import type { BrowserManager } from './browser-manager'; import { consoleBuffer, networkBuffer } from './buffers'; import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Validates that a file path is within allowed directories to prevent path traversal. + * Allowed directories: /tmp, CWD, and subdirectories of CWD. + */ +function validateFilePath(filePath: string): void { + // Reject absolute paths outside /tmp + if (path.isAbsolute(filePath) && !filePath.startsWith('/tmp/')) { + throw new Error(`Security: Absolute paths must be within /tmp. Received: ${filePath}`); + } + + // Resolve the path and check for traversal + const resolvedPath = path.resolve(filePath); + const cwd = process.cwd(); + const tmpDir = '/tmp'; + + if (!resolvedPath.startsWith(tmpDir + '/') && !resolvedPath.startsWith(cwd + '/')) { + throw new Error(`Security: File must be within /tmp or the current working directory. Received: ${filePath}`); + } +} export async function handleReadCommand( command: string, @@ -98,6 +119,7 @@ export async function handleReadCommand( case 'eval': { const filePath = args[0]; if (!filePath) throw new Error('Usage: browse eval '); + validateFilePath(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); From 907c67e9bf93227e09141f72f949e9fe70f48cd7 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 13 Mar 2026 06:50:14 +0800 Subject: [PATCH 2/2] fix: default to sonnet model in browse skill to save tokens The browse skill had no model specified in the frontmatter, causing it to use the user's default model (usually Opus) by default. Since browse is orchestrating CLI commands with minimal reasoning required, Sonnet is more cost-effective while maintaining good performance. This addresses issue #8. --- browse/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/browse/SKILL.md b/browse/SKILL.md index b752aec68..b80e251c8 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -1,6 +1,7 @@ --- name: browse version: 1.0.0 +model: sonnet description: | Fast web browsing for Claude Code via persistent headless Chromium daemon. Navigate to any URL, read page content, click elements, fill forms, run JavaScript, take screenshots,