mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat: centralize content wrapping in handleCommandInternal response path
Single wrapping location replaces fragmented per-handler wrapping: - Scoped tokens: content filters + datamarking + enhanced envelope - Root tokens: existing basic wrapping (backward compat) - Chain subcommands exempt from top-level wrapping (wrapped individually) - Adds 'attrs' to PAGE_CONTENT_COMMANDS (ARIA value exposure defense) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ export const ALL_COMMANDS = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...MET
|
||||
|
||||
/** Commands that return untrusted third-party page content */
|
||||
export const PAGE_CONTENT_COMMANDS = new Set([
|
||||
'text', 'html', 'links', 'forms', 'accessibility',
|
||||
'text', 'html', 'links', 'forms', 'accessibility', 'attrs',
|
||||
'console', 'dialog',
|
||||
]);
|
||||
|
||||
|
||||
+33
-5
@@ -20,6 +20,10 @@ import { handleMetaCommand } from './meta-commands';
|
||||
import { handleCookiePickerRoute } from './cookie-picker-routes';
|
||||
import { sanitizeExtensionUrl } from './sidebar-utils';
|
||||
import { COMMAND_DESCRIPTIONS, PAGE_CONTENT_COMMANDS, wrapUntrustedContent } from './commands';
|
||||
import {
|
||||
wrapUntrustedPageContent, datamarkContent,
|
||||
runContentFilters, type ContentFilterResult,
|
||||
} from './content-security';
|
||||
import { handleSnapshot, SNAPSHOT_FLAGS } from './snapshot';
|
||||
import {
|
||||
initRegistry, validateToken as validateScopedToken, checkScope, checkDomain,
|
||||
@@ -954,11 +958,6 @@ async function handleCommandInternal(
|
||||
|
||||
if (READ_COMMANDS.has(command)) {
|
||||
result = await handleReadCommand(command, args, browserManager);
|
||||
// Content wrapping for page-content commands (scoped vs root handled here)
|
||||
// Chain subcommands: each gets wrapped individually here. Chain result is NOT re-wrapped.
|
||||
if (PAGE_CONTENT_COMMANDS.has(command)) {
|
||||
result = wrapUntrustedContent(result, browserManager.getCurrentUrl());
|
||||
}
|
||||
} else if (WRITE_COMMANDS.has(command)) {
|
||||
result = await handleWriteCommand(command, args, browserManager);
|
||||
} else if (META_COMMANDS.has(command)) {
|
||||
@@ -1002,6 +1001,35 @@ async function handleCommandInternal(
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Centralized content wrapping (single location for all commands) ───
|
||||
// Scoped tokens: content filter + enhanced envelope + datamarking
|
||||
// Root tokens: basic untrusted content wrapper (backward compat)
|
||||
// Chain exempt from top-level wrapping (each subcommand wrapped individually)
|
||||
if (PAGE_CONTENT_COMMANDS.has(command) && command !== 'chain') {
|
||||
const isScoped = tokenInfo && tokenInfo.clientId !== 'root';
|
||||
if (isScoped) {
|
||||
// Run content filters
|
||||
const filterResult: ContentFilterResult = runContentFilters(
|
||||
result, browserManager.getCurrentUrl(), command,
|
||||
);
|
||||
if (filterResult.blocked) {
|
||||
return { status: 403, json: true, result: JSON.stringify({ error: filterResult.message }) };
|
||||
}
|
||||
// Datamark text command output only (not html, forms, or structured data)
|
||||
if (command === 'text') {
|
||||
result = datamarkContent(result);
|
||||
}
|
||||
// Enhanced envelope wrapping for scoped tokens
|
||||
result = wrapUntrustedPageContent(
|
||||
result, command,
|
||||
filterResult.warnings.length > 0 ? filterResult.warnings : undefined,
|
||||
);
|
||||
} else {
|
||||
// Root token: basic wrapping (backward compat, Decision 2)
|
||||
result = wrapUntrustedContent(result, browserManager.getCurrentUrl());
|
||||
}
|
||||
}
|
||||
|
||||
// Activity: emit command_end (skipped for chain subcommands)
|
||||
if (!opts?.skipActivity) {
|
||||
emitActivity({
|
||||
|
||||
Reference in New Issue
Block a user