feat(security): track cookie-imported domains and scope cookie imports

Foundation for origin-pinned JS execution (#616). Tracks which domains
cookies were imported from so the JS/eval commands can verify execution
stays within imported origins.

Changes:
- BrowserManager: new cookieImportedDomains Set with track/get/has methods
- cookie-import: tracks imported cookie domains after addCookies
- cookie-import-browser: tracks domains on --domain direct import
- cookie-import-browser --all: new explicit opt-in for all-domain import
  (previously implicit behavior, now requires deliberate flag)

Closes #615

Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-04-13 09:38:31 -07:00
parent 9b65041d1c
commit f348af4c93
2 changed files with 46 additions and 4 deletions
+16
View File
@@ -55,6 +55,9 @@ export class BrowserManager {
private dialogAutoAccept: boolean = true; private dialogAutoAccept: boolean = true;
private dialogPromptText: string | null = null; private dialogPromptText: string | null = null;
// ─── Cookie Origin Tracking ────────────────────────────────
private cookieImportedDomains: Set<string> = new Set();
// ─── Handoff State ───────────────────────────────────────── // ─── Handoff State ─────────────────────────────────────────
private isHeaded: boolean = false; private isHeaded: boolean = false;
private consecutiveFailures: number = 0; private consecutiveFailures: number = 0;
@@ -749,6 +752,19 @@ export class BrowserManager {
return this.dialogPromptText; return this.dialogPromptText;
} }
// ─── Cookie Origin Tracking ────────────────────────────────
trackCookieImportDomains(domains: string[]): void {
for (const d of domains) this.cookieImportedDomains.add(d);
}
getCookieImportedDomains(): ReadonlySet<string> {
return this.cookieImportedDomains;
}
hasCookieImports(): boolean {
return this.cookieImportedDomains.size > 0;
}
// ─── Viewport ────────────────────────────────────────────── // ─── Viewport ──────────────────────────────────────────────
async setViewport(width: number, height: number) { async setViewport(width: number, height: number) {
await this.getPage().setViewportSize({ width, height }); await this.getPage().setViewportSize({ width, height });
+30 -4
View File
@@ -478,20 +478,24 @@ export async function handleWriteCommand(
} }
await page.context().addCookies(cookies); await page.context().addCookies(cookies);
const importedDomains = [...new Set(cookies.map((c: any) => c.domain).filter(Boolean))];
if (importedDomains.length > 0) bm.trackCookieImportDomains(importedDomains);
return `Loaded ${cookies.length} cookies from ${filePath}`; return `Loaded ${cookies.length} cookies from ${filePath}`;
} }
case 'cookie-import-browser': { case 'cookie-import-browser': {
// Two modes: // Two modes:
// 1. Direct CLI import: cookie-import-browser <browser> --domain <domain> [--profile <profile>] // 1. Direct CLI import: cookie-import-browser <browser> --domain <domain> [--profile <profile>]
// 2. Open picker UI: cookie-import-browser [browser] // Requires --domain (or --all to explicitly import everything).
// 2. Open picker UI: cookie-import-browser [browser] (interactive domain selection)
const browserArg = args[0]; const browserArg = args[0];
const domainIdx = args.indexOf('--domain'); const domainIdx = args.indexOf('--domain');
const profileIdx = args.indexOf('--profile'); const profileIdx = args.indexOf('--profile');
const hasAll = args.includes('--all');
const profile = (profileIdx !== -1 && profileIdx + 1 < args.length) ? args[profileIdx + 1] : 'Default'; const profile = (profileIdx !== -1 && profileIdx + 1 < args.length) ? args[profileIdx + 1] : 'Default';
if (domainIdx !== -1 && domainIdx + 1 < args.length) { if (domainIdx !== -1 && domainIdx + 1 < args.length) {
// Direct import mode — no UI // Direct import mode — scoped to specific domain
const domain = args[domainIdx + 1]; const domain = args[domainIdx + 1];
// Validate --domain against current page hostname to prevent cross-site cookie injection // Validate --domain against current page hostname to prevent cross-site cookie injection
const pageHostname = new URL(page.url()).hostname; const pageHostname = new URL(page.url()).hostname;
@@ -503,13 +507,35 @@ export async function handleWriteCommand(
const result = await importCookies(browser, [domain], profile); const result = await importCookies(browser, [domain], profile);
if (result.cookies.length > 0) { if (result.cookies.length > 0) {
await page.context().addCookies(result.cookies); await page.context().addCookies(result.cookies);
bm.trackCookieImportDomains([domain]);
} }
const msg = [`Imported ${result.count} cookies for ${domain} from ${browser}`]; const msg = [`Imported ${result.count} cookies for ${domain} from ${browser}`];
if (result.failed > 0) msg.push(`(${result.failed} failed to decrypt)`); if (result.failed > 0) msg.push(`(${result.failed} failed to decrypt)`);
return msg.join(' '); return msg.join(' ');
} }
// Picker UI mode — open in user's browser if (hasAll) {
// Explicit all-cookies import — requires --all flag as a deliberate opt-in.
// Imports every non-expired cookie domain from the browser.
const browser = browserArg || 'comet';
const { listDomains } = await import('./cookie-import-browser');
const { domains } = listDomains(browser, profile);
const allDomainNames = domains.map((d: any) => d.domain);
if (allDomainNames.length === 0) {
return `No cookies found in ${browser} (profile: ${profile})`;
}
const result = await importCookies(browser, allDomainNames, profile);
if (result.cookies.length > 0) {
await page.context().addCookies(result.cookies);
bm.trackCookieImportDomains(allDomainNames);
}
const msg = [`Imported ${result.count} cookies across ${Object.keys(result.domainCounts).length} domains from ${browser}`];
msg.push('(used --all: all browser cookies imported, consider --domain for tighter scoping)');
if (result.failed > 0) msg.push(`(${result.failed} failed to decrypt)`);
return msg.join(' ');
}
// Picker UI mode — open in user's browser for interactive domain selection
const port = bm.serverPort; const port = bm.serverPort;
if (!port) throw new Error('Server port not available'); if (!port) throw new Error('Server port not available');
@@ -527,7 +553,7 @@ export async function handleWriteCommand(
if (err?.code !== 'ENOENT' && !err?.message?.includes('spawn')) throw err; if (err?.code !== 'ENOENT' && !err?.message?.includes('spawn')) throw err;
} }
return `Cookie picker opened at http://127.0.0.1:${port}/cookie-picker\nDetected browsers: ${browsers.map(b => b.name).join(', ')}\nSelect domains to import, then close the picker when done.`; return `Cookie picker opened at http://127.0.0.1:${port}/cookie-picker\nDetected browsers: ${browsers.map(b => b.name).join(', ')}\nSelect domains to import, then close the picker when done.\n\nTip: For scripted imports, use --domain <domain> to scope cookies to a single domain.`;
} }
case 'style': { case 'style': {