mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-10 01:48:38 +02:00
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:
@@ -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 });
|
||||||
|
|||||||
@@ -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': {
|
||||||
|
|||||||
Reference in New Issue
Block a user