mirror of https://github.com/garrytan/gstack.git
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:
parent
50d7b5fa1c
commit
053b46e371
|
|
@ -48,7 +48,11 @@ export const PAGE_CONTENT_COMMANDS = new Set([
|
||||||
|
|
||||||
/** Wrap output from untrusted-content commands with trust boundary markers */
|
/** Wrap output from untrusted-content commands with trust boundary markers */
|
||||||
export function wrapUntrustedContent(result: string, url: string): string {
|
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 }> = {
|
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 ─────────────────────────────────────
|
// ─── Snapshot ─────────────────────────────────────
|
||||||
|
|
@ -310,7 +310,7 @@ export async function handleMetaCommand(
|
||||||
bm.resume();
|
bm.resume();
|
||||||
// Re-snapshot to capture current page state after human interaction
|
// Re-snapshot to capture current page state after human interaction
|
||||||
const snapshot = await handleSnapshot(['-i'], bm);
|
const snapshot = await handleSnapshot(['-i'], bm);
|
||||||
return `RESUMED\n${snapshot}`;
|
return `RESUMED\n${wrapUntrustedContent(snapshot, bm.getCurrentUrl())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Headed Mode ──────────────────────────────────────
|
// ─── Headed Mode ──────────────────────────────────────
|
||||||
|
|
@ -381,11 +381,14 @@ export async function handleMetaCommand(
|
||||||
if (!bm.isWatching()) return 'Not currently watching.';
|
if (!bm.isWatching()) return 'Not currently watching.';
|
||||||
const result = bm.stopWatch();
|
const result = bm.stopWatch();
|
||||||
const durationSec = Math.round(result.duration / 1000);
|
const durationSec = Math.round(result.duration / 1000);
|
||||||
|
const lastSnapshot = result.snapshots.length > 0
|
||||||
|
? wrapUntrustedContent(result.snapshots[result.snapshots.length - 1], bm.getCurrentUrl())
|
||||||
|
: '(none)';
|
||||||
return [
|
return [
|
||||||
`WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`,
|
`WATCH STOPPED (${durationSec}s, ${result.snapshots.length} snapshots)`,
|
||||||
'',
|
'',
|
||||||
'Last snapshot:',
|
'Last snapshot:',
|
||||||
result.snapshots.length > 0 ? result.snapshots[result.snapshots.length - 1] : '(none)',
|
lastSnapshot,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue