diff --git a/browse/src/cookie-import-browser.ts b/browse/src/cookie-import-browser.ts index 5a60c6aa..29d9db3e 100644 --- a/browse/src/cookie-import-browser.ts +++ b/browse/src/cookie-import-browser.ts @@ -241,7 +241,7 @@ function openDb(dbPath: string, browserName: string): Database { } function openDbFromCopy(dbPath: string, browserName: string): Database { - const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}.db`; + const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`; try { fs.copyFileSync(dbPath, tmpPath); // Also copy WAL and SHM if they exist (for consistent reads) diff --git a/browse/src/meta-commands.ts b/browse/src/meta-commands.ts index b9b39848..8d3f9ebe 100644 --- a/browse/src/meta-commands.ts +++ b/browse/src/meta-commands.ts @@ -30,7 +30,7 @@ const CHAIN_READ = new Set([ const CHAIN_WRITE = new Set([ 'goto', 'back', 'forward', 'reload', 'click', 'fill', 'select', 'hover', 'type', 'press', 'scroll', 'wait', - 'viewport', 'cookie', 'header', 'useragent', + 'viewport', 'cookie', 'cookie-import', 'header', 'useragent', 'upload', 'dialog-accept', 'dialog-dismiss', 'cookie-import-browser', ]); diff --git a/browse/src/snapshot.ts b/browse/src/snapshot.ts index 65836657..b0c7b80f 100644 --- a/browse/src/snapshot.ts +++ b/browse/src/snapshot.ts @@ -309,6 +309,12 @@ export async function handleSnapshot( // ─── Annotated screenshot (-a) ──────────────────────────── if (opts.annotate) { const screenshotPath = opts.outputPath || '/tmp/browse-annotated.png'; + // Validate output path (consistent with screenshot/pdf/responsive) + const resolvedPath = require('path').resolve(screenshotPath); + const safeDirs = ['/tmp', process.cwd()]; + if (!safeDirs.some((dir: string) => resolvedPath === dir || resolvedPath.startsWith(dir + '/'))) { + throw new Error(`Path must be within: ${safeDirs.join(', ')}`); + } try { // Inject overlay divs at each ref's bounding box const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number } }> = []; diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 70478ea4..71ce3dc8 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -1523,4 +1523,38 @@ describe('Path traversal prevention', () => { expect(err.message).toContain('Path must be within'); } }); + + test('snapshot -a -o rejects path outside safe dirs', async () => { + await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm); + // First get a snapshot so refs exist + await handleMetaCommand('snapshot', ['-i'], bm, () => {}); + try { + await handleMetaCommand('snapshot', ['-a', '-o', '/etc/evil.png'], bm, () => {}); + expect(true).toBe(false); + } catch (err: any) { + expect(err.message).toContain('Path must be within'); + } + }); +}); + +// ─── Chain command: cookie-import in chain ────────────────────── + +describe('Chain with cookie-import', () => { + test('cookie-import works inside chain', async () => { + await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm); + const tmpCookies = '/tmp/test-chain-cookies.json'; + fs.writeFileSync(tmpCookies, JSON.stringify([ + { name: 'chain_test', value: 'chain_value', domain: 'localhost', path: '/' } + ])); + try { + const commands = JSON.stringify([ + ['cookie-import', tmpCookies], + ]); + const result = await handleMetaCommand('chain', [commands], bm, async () => {}); + expect(result).toContain('[cookie-import]'); + expect(result).toContain('Loaded 1 cookie'); + } finally { + try { fs.unlinkSync(tmpCookies); } catch {} + } + }); }); diff --git a/package.json b/package.json index 9f3bb207..bc617a53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "0.0.1", + "version": "0.3.1", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module",