This commit is contained in:
RoomWithOutRoof 2026-03-13 06:51:15 +08:00 committed by GitHub
commit 4af23dd75b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 0 deletions

View File

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

View File

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

View File

@ -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 <js-file>');
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);