mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 08:29:04 +02:00
fix(security): delete chain's shadow dispatcher that skipped every security gate
meta-commands.ts carried a 'CLI mode' fallback that re-implemented command
routing without the server pipeline's gates: no scope check, no domain check,
no tab ownership, no rate limit, no hidden-element stripping, no scoped-token
enveloping — and it called handleReadCommand without a BrowserManager, which
also skipped the JS-origin cookie-exfiltration assertion. It was unreachable
in production (server.ts always passes executeCommand) and one boolean away
from being live.
chain now hard-errors without a server context. handleReadCommand's bm param
is required and assertJsOriginAllowed runs unconditionally. The chain tests
that exercised the deleted fallback now route through a server-shaped
executeCommand adapter (real handlers + trust wrapping + {status,result}
envelope), so their behavioral coverage — sequencing, trust markers, pipe
format, aliases, error reporting — survives on the production-shaped path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b241a79ee5
commit
ef03646e07
@@ -667,40 +667,13 @@ export async function handleMetaCommand(
|
||||
lastWasWrite = WRITE_COMMANDS.has(c.name);
|
||||
}
|
||||
} else {
|
||||
// Fallback: direct dispatch (CLI mode, no server context)
|
||||
const { handleReadCommand } = await import('./read-commands');
|
||||
const { handleWriteCommand } = await import('./write-commands');
|
||||
|
||||
for (const c of commands) {
|
||||
const name = c.name;
|
||||
const cmdArgs = c.args;
|
||||
const label = c.rawName === name ? name : `${c.rawName}→${name}`;
|
||||
try {
|
||||
let result: string;
|
||||
if (WRITE_COMMANDS.has(name)) {
|
||||
if (bm.isWatching()) {
|
||||
result = 'BLOCKED: write commands disabled in watch mode';
|
||||
} else {
|
||||
result = await handleWriteCommand(name, cmdArgs, session, bm);
|
||||
}
|
||||
lastWasWrite = true;
|
||||
} else if (READ_COMMANDS.has(name)) {
|
||||
result = await handleReadCommand(name, cmdArgs, session);
|
||||
if (PAGE_CONTENT_COMMANDS.has(name)) {
|
||||
result = wrapUntrustedContent(result, bm.getCurrentUrl());
|
||||
}
|
||||
lastWasWrite = false;
|
||||
} else if (META_COMMANDS.has(name)) {
|
||||
result = await handleMetaCommand(name, cmdArgs, bm, shutdown, tokenInfo, opts);
|
||||
lastWasWrite = false;
|
||||
} else {
|
||||
throw new Error(`Unknown command: ${c.rawName}`);
|
||||
}
|
||||
results.push(`[${label}] ${result}`);
|
||||
} catch (err: any) {
|
||||
results.push(`[${label}] ERROR: ${err.message}`);
|
||||
}
|
||||
}
|
||||
// No fallback dispatcher. The old direct-dispatch branch here
|
||||
// re-implemented command routing WITHOUT the server pipeline's
|
||||
// security gates (scope, domain, tab ownership, rate limit, hidden
|
||||
// element stripping, scoped-token enveloping, JS-origin assertion).
|
||||
// It was unreachable in production (server.ts always passes
|
||||
// executeCommand) and one boolean away from being live.
|
||||
throw new Error('chain requires the browse server (no executeCommand context)');
|
||||
}
|
||||
|
||||
// Wait for network to settle after write commands before returning
|
||||
|
||||
@@ -212,7 +212,7 @@ export async function handleReadCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
session: TabSession,
|
||||
bm?: BrowserManager,
|
||||
bm: BrowserManager,
|
||||
): Promise<string> {
|
||||
const page = session.getPage();
|
||||
// Frame-aware target for content extraction
|
||||
@@ -293,7 +293,7 @@ export async function handleReadCommand(
|
||||
const { outPath, raw, rest } = parseOutArgs(args);
|
||||
const expr = rest[0];
|
||||
if (!expr) throw new Error('Usage: browse js <expression> [--out <file>] [--raw]');
|
||||
if (bm) assertJsOriginAllowed(bm, page.url());
|
||||
assertJsOriginAllowed(bm, page.url());
|
||||
const wrapped = wrapForEvaluate(expr);
|
||||
const result = await target.evaluate(wrapped);
|
||||
const str = resultToString(result);
|
||||
@@ -308,7 +308,7 @@ export async function handleReadCommand(
|
||||
const { outPath, raw, rest } = parseOutArgs(args);
|
||||
const filePath = rest[0];
|
||||
if (!filePath) throw new Error('Usage: browse eval <js-file> [--out <file>] [--raw]');
|
||||
if (bm) assertJsOriginAllowed(bm, page.url());
|
||||
assertJsOriginAllowed(bm, page.url());
|
||||
validateReadPath(filePath);
|
||||
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
||||
const code = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
Reference in New Issue
Block a user