mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
feat(security): pin JS/eval execution to cookie-imported origins
When cookies have been imported for specific domains, block JS execution on pages whose origin doesn't match. Prevents the attack chain: 1. Agent imports cookies for github.com 2. Prompt injection navigates to attacker.com 3. Agent runs js document.cookie → exfiltrates github cookies assertJsOriginAllowed() checks the current page hostname against imported cookie domains with subdomain matching (.github.com allows api.github.com). When no cookies are imported, all origins allowed (nothing to protect). about:blank and data: URIs are allowed (no cookies at risk). Depends on #615 (cookie domain tracking). Closes #616 Co-Authored-By: Alberto Martinez <halbert04@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { TabSession } from './tab-session';
|
||||
import type { BrowserManager } from './browser-manager';
|
||||
import { consoleBuffer, networkBuffer, dialogBuffer } from './buffers';
|
||||
import type { Page, Frame } from 'playwright';
|
||||
import * as fs from 'fs';
|
||||
@@ -62,10 +63,43 @@ export async function getCleanText(page: Page | Frame): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When cookies have been imported for specific domains, block JS execution
|
||||
* on pages whose origin doesn't match any imported cookie domain.
|
||||
* Prevents cross-origin cookie exfiltration via `js document.cookie` or
|
||||
* similar when the agent navigates to an untrusted page.
|
||||
*/
|
||||
function assertJsOriginAllowed(bm: BrowserManager, pageUrl: string): void {
|
||||
if (!bm.hasCookieImports()) return;
|
||||
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(pageUrl).hostname;
|
||||
} catch {
|
||||
return; // about:blank, data: URIs — allow (no cookies at risk)
|
||||
}
|
||||
|
||||
const importedDomains = bm.getCookieImportedDomains();
|
||||
const allowed = [...importedDomains].some(domain => {
|
||||
// Exact match or subdomain match (e.g., ".github.com" matches "api.github.com")
|
||||
const normalized = domain.startsWith('.') ? domain : '.' + domain;
|
||||
return hostname === domain.replace(/^\./, '') || hostname.endsWith(normalized);
|
||||
});
|
||||
|
||||
if (!allowed) {
|
||||
throw new Error(
|
||||
`JS execution blocked: current page (${hostname}) does not match any cookie-imported domain. ` +
|
||||
`Imported cookies for: ${[...importedDomains].join(', ')}. ` +
|
||||
`This prevents cross-origin cookie exfiltration. Navigate to an imported domain or run without imported cookies.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleReadCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
session: TabSession
|
||||
session: TabSession,
|
||||
bm?: BrowserManager,
|
||||
): Promise<string> {
|
||||
const page = session.getPage();
|
||||
// Frame-aware target for content extraction
|
||||
@@ -145,6 +179,7 @@ export async function handleReadCommand(
|
||||
case 'js': {
|
||||
const expr = args[0];
|
||||
if (!expr) throw new Error('Usage: browse js <expression>');
|
||||
if (bm) assertJsOriginAllowed(bm, page.url());
|
||||
const wrapped = wrapForEvaluate(expr);
|
||||
const result = await target.evaluate(wrapped);
|
||||
return typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
|
||||
@@ -153,6 +188,7 @@ export async function handleReadCommand(
|
||||
case 'eval': {
|
||||
const filePath = args[0];
|
||||
if (!filePath) throw new Error('Usage: browse eval <js-file>');
|
||||
if (bm) assertJsOriginAllowed(bm, page.url());
|
||||
validateReadPath(filePath);
|
||||
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
||||
const code = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
@@ -1013,7 +1013,7 @@ async function handleCommandInternal(
|
||||
await cleanupHiddenMarkers(page);
|
||||
}
|
||||
} else {
|
||||
result = await handleReadCommand(command, args, session);
|
||||
result = await handleReadCommand(command, args, session, browserManager);
|
||||
}
|
||||
} else if (WRITE_COMMANDS.has(command)) {
|
||||
result = await handleWriteCommand(command, args, session, browserManager);
|
||||
|
||||
Reference in New Issue
Block a user