From 2e54d0d1713d486eec17b9c25c7ef8d8f209b6af Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:57:41 +0200 Subject: [PATCH] fix: re-sanitize after scrubbing style blocks to block markup reintroduction scrubStyleBlocks runs after sanitizeHtml and splices un-escaped CSS back as raw markup, so an escaped sequence like \3c/style\3e\3cimage/\3e (inert text during the first pass) became a real element after unescaping, past the allowlist. Re-run the allowlist over the scrubbed result whenever a '; + const clean = sanitizeSvg(dirty); + expect(clean.toLowerCase()).not.toContain(' { const attrEsc = sanitizeSvg( '', diff --git a/packages/api/src/mcp/icons.spec.ts b/packages/api/src/mcp/icons.spec.ts index 65091e181e..87bebe453f 100644 --- a/packages/api/src/mcp/icons.spec.ts +++ b/packages/api/src/mcp/icons.spec.ts @@ -131,6 +131,22 @@ describe('sanitizeMcpIconPath', () => { expect(clean).not.toContain('alert(1)'); }); + it('does not let escaped markup in a stylesheet reintroduce elements past the allowlist', () => { + // `\3c` = "<", `\3e` = ">": harmless text during the first pass, real markup + // once the CSS is un-escaped and spliced back — must be re-sanitized away. + const cases = [ + '', + '', + '', + ]; + for (const raw of cases) { + const clean = decode(sanitizeMcpIconPath(`data:image/svg+xml,${encodeURIComponent(raw)}`)); + expect(clean).not.toContain('evil.example'); + expect(clean.toLowerCase()).not.toContain(' { for (const attr of [ 'filter="url(https://evil.example/f.svg#f)"', diff --git a/packages/api/src/mcp/icons.ts b/packages/api/src/mcp/icons.ts index e495679a5c..b7eebc6c8c 100644 --- a/packages/api/src/mcp/icons.ts +++ b/packages/api/src/mcp/icons.ts @@ -362,7 +362,13 @@ export function sanitizeMcpIconPath(iconPath: string): string { if (svg == null) { return ''; } - const clean = scrubStyleBlocks(sanitizeHtml(svg, SVG_SANITIZE_OPTIONS)); + const sanitized = sanitizeHtml(svg, SVG_SANITIZE_OPTIONS); + const scrubbed = scrubStyleBlocks(sanitized); + /* `scrubStyleBlocks` splices un-escaped CSS back as raw markup, so an escaped + * sequence like `\3c/style\3e\3cimage/\3e` can reintroduce a real element past + * the allowlist. When a `