mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
fix: harden trust boundary markers against escape attacks
- Sanitize URLs in markers (remove newlines, cap at 200 chars) to prevent marker injection via history.pushState - Escape marker strings in content (zero-width space) so malicious pages can't forge the END marker to break out of the untrusted block - Wrap resume command snapshot with trust boundary markers - Wrap diff command output with trust boundary markers - Wrap watch stop last snapshot with trust boundary markers Found by cross-model adversarial review (Claude + Codex).
This commit is contained in:
@@ -48,7 +48,11 @@ export const PAGE_CONTENT_COMMANDS = new Set([
|
||||
|
||||
/** Wrap output from untrusted-content commands with trust boundary markers */
|
||||
export function wrapUntrustedContent(result: string, url: string): string {
|
||||
return `--- BEGIN UNTRUSTED EXTERNAL CONTENT (source: ${url}) ---\n${result}\n--- END UNTRUSTED EXTERNAL CONTENT ---`;
|
||||
// Sanitize URL: remove newlines to prevent marker injection via history.pushState
|
||||
const safeUrl = url.replace(/[\n\r]/g, '').slice(0, 200);
|
||||
// Escape marker strings in content to prevent boundary escape attacks
|
||||
const safeResult = result.replace(/--- (BEGIN|END) UNTRUSTED EXTERNAL CONTENT/g, '--- $1 UNTRUSTED EXTERNAL C\u200BONTENT');
|
||||
return `--- BEGIN UNTRUSTED EXTERNAL CONTENT (source: ${safeUrl}) ---\n${safeResult}\n--- END UNTRUSTED EXTERNAL CONTENT ---`;
|
||||
}
|
||||
|
||||
export const COMMAND_DESCRIPTIONS: Record<string, { category: string; description: string; usage?: string }> = {
|
||||
|
||||
@@ -291,7 +291,7 @@ export async function handleMetaCommand(
|
||||
}
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
return wrapUntrustedContent(output.join('\n'), `diff: ${url1} vs ${url2}`);
|
||||
}
|
||||
|
||||
// ─── Snapshot ─────────────────────────────────────
|
||||
@@ -310,7 +310,7 @@ export async function handleMetaCommand(
|
||||
bm.resume();
|
||||
// Re-snapshot to capture current page state after human interaction
|
||||
const snapshot = await handleSnapshot(['-i'], bm);
|
||||
return `RESUMED\n${snapshot}`;
|
||||
return `RESUMED\n${wrapUntrustedContent(snapshot, bm.getCurrentUrl())}`;
|
||||
}
|
||||
|
||||
// ─── Headed Mode ──────────────────────────────────────
|
||||
@@ -381,11 +381,14 @@ export async function handleMetaCommand(
|
||||
if (!bm.isWatching()) return 'Not currently watching.';
|
||||
const result = bm.stopWatch();
|
||||
const durationSec = Math.round(result.duration / 1000);
|
||||
const lastSnapshot = result.snapshots.length > 0
|
||||
? wrapUntrustedContent(result.snapshots[result.snapshots.length - 1], bm.getCurrentUrl())
|
||||
: '(none)';
|
||||
return [
|
||||
`WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`,
|
||||
'',
|
||||
'Last snapshot:',
|
||||
result.snapshots.length > 0 ? result.snapshots[result.snapshots.length - 1] : '(none)',
|
||||
lastSnapshot,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user