security: remove .svg from load-html extension allowlist

SVG files can contain inline <script> tags, event handlers (onload,
onclick), and <foreignObject> elements. When loaded via setContent(),
JavaScript executes in an about:blank context with full fetch() access
to localhost — including the gstack server itself.

Attack vector: a malicious SVG placed in a project directory (via git
clone, npm package, or download) gets loaded by the AI agent through
load-html. The SVG's JavaScript modifies the DOM to inject prompt
injection payloads that the agent reads on subsequent text/snapshot
commands.

SVG rendering is still available via `goto file://path/to/file.svg`
which goes through validateNavigationUrl + validateReadPath with
safe-dirs enforcement.
This commit is contained in:
gus 2026-04-22 19:54:42 -03:00
parent 675717e320
commit 3095f39ef5
2 changed files with 15 additions and 2 deletions

View File

@ -247,11 +247,11 @@ export async function handleWriteCommand(
if (!filePath) throw new Error('Usage: browse load-html <file> [--wait-until load|domcontentloaded|networkidle] [--tab-id <N>] | load-html --from-file <payload.json> [--tab-id <N>]');
// Extension allowlist
const ALLOWED_EXT = ['.html', '.htm', '.xhtml', '.svg'];
const ALLOWED_EXT = ['.html', '.htm', '.xhtml'];
const ext = path.extname(filePath).toLowerCase();
if (!ALLOWED_EXT.includes(ext)) {
throw new Error(
`load-html: file does not appear to be HTML. Expected .html/.htm/.xhtml/.svg, got ${ext || '(no extension)'}. Rename the file if it's really HTML.`
`load-html: file does not appear to be HTML. Expected .html/.htm/.xhtml, got ${ext || '(no extension)'}. Rename the file if it's really HTML.`
);
}

View File

@ -2143,6 +2143,19 @@ describe('load-html', () => {
}
});
test('load-html rejects .svg files', async () => {
const svgPath = path.join(tmpDir, `load-html-test-${Date.now()}.svg`);
fs.writeFileSync(svgPath, '<svg xmlns="http://www.w3.org/2000/svg"><text>hi</text></svg>');
try {
await handleWriteCommand('load-html', [svgPath], bm);
expect(true).toBe(false);
} catch (err: any) {
expect(err.message).toMatch(/does not appear to be HTML/);
} finally {
try { fs.unlinkSync(svgPath); } catch {}
}
});
test('load-html rejects file outside safe dirs', async () => {
try {
await handleWriteCommand('load-html', ['/etc/passwd.html'], bm);