mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-18 20:17:14 +02:00
fix: chain scope bypass + /health info leak when tunneled
1. Chain command now pre-validates ALL subcommand scopes before executing any. A read+meta token can no longer escalate to admin via chain (eval, js, cookies were dispatched without scope checks). tokenInfo flows through handleMetaCommand into the chain handler. Rejects entire chain if any subcommand fails. 2. /health strips sensitive fields (currentUrl, agent.currentMessage, session) when tunnel is active. Only operational metadata (status, mode, uptime, tabs) exposed to the internet. Previously anyone reaching the ngrok URL could surveil browsing activity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a5b40045b8
commit
36a20c5d59
@@ -7,6 +7,7 @@ import { handleSnapshot } from './snapshot';
|
||||
import { getCleanText } from './read-commands';
|
||||
import { READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent } from './commands';
|
||||
import { validateNavigationUrl } from './url-validation';
|
||||
import { checkScope, type TokenInfo } from './token-registry';
|
||||
import * as Diff from 'diff';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -48,7 +49,8 @@ export async function handleMetaCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
bm: BrowserManager,
|
||||
shutdown: () => Promise<void> | void
|
||||
shutdown: () => Promise<void> | void,
|
||||
tokenInfo?: TokenInfo | null
|
||||
): Promise<string> {
|
||||
switch (command) {
|
||||
// ─── Tabs ──────────────────────────────────────────
|
||||
@@ -232,6 +234,21 @@ export async function handleMetaCommand(
|
||||
const { handleReadCommand } = await import('./read-commands');
|
||||
const { handleWriteCommand } = await import('./write-commands');
|
||||
|
||||
// Pre-validate ALL subcommands against the token's scope before executing any.
|
||||
// This prevents partial execution where some subcommands succeed before a
|
||||
// scope violation is hit, leaving the browser in an inconsistent state.
|
||||
if (tokenInfo && tokenInfo.clientId !== 'root') {
|
||||
for (const cmd of commands) {
|
||||
const [name] = cmd;
|
||||
if (!checkScope(tokenInfo, name)) {
|
||||
throw new Error(
|
||||
`Chain rejected: subcommand "${name}" not allowed by your token scope (${tokenInfo.scopes.join(', ')}). ` +
|
||||
`All subcommands must be within scope.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastWasWrite = false;
|
||||
for (const cmd of commands) {
|
||||
const [name, ...cmdArgs] = cmd;
|
||||
@@ -247,7 +264,7 @@ export async function handleMetaCommand(
|
||||
}
|
||||
lastWasWrite = false;
|
||||
} else if (META_COMMANDS.has(name)) {
|
||||
result = await handleMetaCommand(name, cmdArgs, bm, shutdown);
|
||||
result = await handleMetaCommand(name, cmdArgs, bm, shutdown, tokenInfo);
|
||||
lastWasWrite = false;
|
||||
} else {
|
||||
throw new Error(`Unknown command: ${name}`);
|
||||
|
||||
Reference in New Issue
Block a user