fix(security): cookie-import path validation bypass + hardcoded /tmp

Two fixes:
1. cookie-import relative path bypass (#707): path.isAbsolute() gated the
   entire validation, so relative paths like "sensitive-file.json" bypassed
   the safe-directory check entirely. Now always resolves to absolute path
   with realpathSync for symlink resolution, matching validateOutputPath().

2. Hardcoded /tmp in cookie-import-browser (#708): openDbFromCopy used
   /tmp directly instead of os.tmpdir(), breaking Windows support.

Also adds explicit imports for SAFE_DIRECTORIES and isPathWithin in
write-commands.ts (previously resolved implicitly through bundler).

Closes #852

Co-Authored-By: Toby Morning <urbantech@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-04-13 09:33:48 -07:00
parent ffb56b556d
commit bc80c4fae5
3 changed files with 16 additions and 12 deletions
+2 -1
View File
@@ -386,7 +386,8 @@ function openDb(dbPath: string, browserName: string): Database {
} }
function openDbFromCopy(dbPath: string, browserName: string): Database { function openDbFromCopy(dbPath: string, browserName: string): Database {
const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`; // Use os.tmpdir() instead of hardcoded /tmp for cross-platform support (#708)
const tmpPath = path.join(os.tmpdir(), `browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`);
try { try {
fs.copyFileSync(dbPath, tmpPath); fs.copyFileSync(dbPath, tmpPath);
// Also copy WAL and SHM if they exist (for consistent reads) // Also copy WAL and SHM if they exist (for consistent reads)
+12 -10
View File
@@ -13,7 +13,8 @@ import { validateNavigationUrl } from './url-validation';
import { validateOutputPath } from './path-security'; import { validateOutputPath } from './path-security';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { TEMP_DIR } from './platform'; import { TEMP_DIR, isPathWithin } from './platform';
import { SAFE_DIRECTORIES } from './path-security';
import { modifyStyle, undoModification, resetModifications, getModificationHistory } from './cdp-inspector'; import { modifyStyle, undoModification, resetModifications, getModificationHistory } from './cdp-inspector';
/** /**
@@ -441,16 +442,17 @@ export async function handleWriteCommand(
case 'cookie-import': { case 'cookie-import': {
const filePath = args[0]; const filePath = args[0];
if (!filePath) throw new Error('Usage: browse cookie-import <json-file>'); if (!filePath) throw new Error('Usage: browse cookie-import <json-file>');
// Path validation — prevent reading arbitrary files // Path validation — resolve to absolute and check against safe dirs.
if (path.isAbsolute(filePath)) { // Fixes #707: relative paths previously bypassed the safe directory check.
const safeDirs = [TEMP_DIR, process.cwd()]; // Mirrors validateOutputPath() — resolves symlinks (e.g., macOS /tmp → /private/tmp).
const resolved = path.resolve(filePath); const resolved = path.resolve(filePath);
if (!safeDirs.some(dir => isPathWithin(resolved, dir))) { let resolvedReal = resolved;
throw new Error(`Path must be within: ${safeDirs.join(', ')}`); try { resolvedReal = fs.realpathSync(resolved); } catch {
} // File may not exist yet — resolve parent dir instead
try { resolvedReal = path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved)); } catch {}
} }
if (path.normalize(filePath).includes('..')) { if (!SAFE_DIRECTORIES.some(dir => isPathWithin(resolvedReal, dir))) {
throw new Error('Path traversal sequences (..) are not allowed'); throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
} }
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`); if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
const raw = fs.readFileSync(filePath, 'utf-8'); const raw = fs.readFileSync(filePath, 'utf-8');
+2 -1
View File
@@ -1811,7 +1811,8 @@ describe('Path traversal prevention', () => {
await handleWriteCommand('cookie-import', ['../../etc/shadow'], bm); await handleWriteCommand('cookie-import', ['../../etc/shadow'], bm);
expect(true).toBe(false); expect(true).toBe(false);
} catch (err: any) { } catch (err: any) {
expect(err.message).toContain('Path traversal'); // Traversal blocked by safe-directory check (#707) or explicit .. check
expect(err.message).toMatch(/Path must be within|Path traversal/);
} }
}); });