security: redact sensitive values from command output (PR #21)

type no longer echoes text (reports character count), cookie redacts
value with ****, header redacts Authorization/Cookie/X-API-Key/X-Auth-Token,
storage set drops value, forms redacts password fields. Prevents secrets
from persisting in LLM transcripts. 7 new tests.

Credit: fredluz (PR #21)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-12 20:28:48 -07:00
parent 2b1add81c5
commit 96c3097573
3 changed files with 156 additions and 5 deletions

View File

@@ -9,6 +9,23 @@ import type { BrowserManager } from './browser-manager';
import { consoleBuffer, networkBuffer, dialogBuffer } from './buffers';
import type { Page } from 'playwright';
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 validateReadPath(filePath: string): void {
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(', ')}`);
}
}
const normalized = path.normalize(filePath);
if (normalized.includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
}
}
/**
* Extract clean text from a page (strips script/style/noscript/svg).
@@ -74,7 +91,7 @@ export async function handleReadCommand(
id: input.id || undefined,
placeholder: input.placeholder || undefined,
required: input.required || undefined,
value: input.value || undefined,
value: input.type === 'password' ? '[redacted]' : (input.value || undefined),
options: el.tagName === 'SELECT'
? [...(el as HTMLSelectElement).options].map(o => ({ value: o.value, text: o.text }))
: undefined,
@@ -107,6 +124,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);
@@ -238,7 +256,7 @@ export async function handleReadCommand(
const key = args[1];
const value = args[2] || '';
await page.evaluate(([k, v]) => localStorage.setItem(k, v), [key, value]);
return `Set localStorage["${key}"] = "${value}"`;
return `Set localStorage["${key}"]`;
}
const storage = await page.evaluate(() => ({
localStorage: { ...localStorage },

View File

@@ -97,7 +97,7 @@ export async function handleWriteCommand(
const text = args.join(' ');
if (!text) throw new Error('Usage: browse type <text>');
await page.keyboard.type(text);
return `Typed "${text}"`;
return `Typed ${text.length} characters`;
}
case 'press': {
@@ -169,7 +169,7 @@ export async function handleWriteCommand(
domain: url.hostname,
path: '/',
}]);
return `Cookie set: ${name}=${value}`;
return `Cookie set: ${name}=****`;
}
case 'header': {
@@ -179,7 +179,9 @@ export async function handleWriteCommand(
const name = headerStr.slice(0, sep).trim();
const value = headerStr.slice(sep + 1).trim();
await bm.setExtraHeader(name, value);
return `Header set: ${name}: ${value}`;
const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token'];
const redactedValue = sensitiveHeaders.includes(name.toLowerCase()) ? '****' : value;
return `Header set: ${name}: ${redactedValue}`;
}
case 'useragent': {