From 5278eb906ebecefc6538a19bc86df09d997e43e6 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:03:30 +0200 Subject: [PATCH] fix: block external resource loading in Vega chart rendering to prevent client-side SSRF (#26806) * fix: block external resource loading in Vega chart rendering to prevent client-side SSRF renderVegaVisualization renders vega/vega-lite chart specs that appear in untrusted chat content (shared chats, channel messages, assistant/RAG/tool output) by constructing a Vega View with no restricted loader, so a crafted spec could make a viewer's browser issue arbitrary outbound requests. There are two paths: data.url (and topojson/geo data) is fetched via loader.load at view construction, and image-mark urls are resolved via loader.sanitize and emitted as into the output SVG, fetched by the browser when the SVG is displayed. Both are client-side SSRF, and against same-origin or CORS-permissive targets allow reading the response back into the page. Pass a loader that rejects external resource loads on both paths, load throws and sanitize rejects http(s)/protocol-relative URIs, so rendered charts can only use inline data. Inline data.values charts are unaffected. Co-authored-by: Zureno * fix: resolve Vega image urls with the URL parser before blocking external loads The previous scheme regex could be bypassed with encodings the browser URL parser normalizes away, such as a leading tab or newline before the scheme and backslash variants of protocol-relative urls like /\evil.com, which would still be emitted into the rendered SVG and fetched externally on display. Resolve the uri against document.baseURI with the browser's own URL parser and only allow data: uris and same-origin results, so the check cannot diverge from what the browser would actually fetch. Also shortens the explanatory comments. --------- Co-authored-by: Zureno --- src/lib/utils/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index eb23447b03..f4f822b6e6 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -2049,7 +2049,22 @@ export const renderVegaVisualization = async (spec: string, lang: string = '', i const vegaLite = await import('vega-lite'); vegaSpec = vegaLite.compile(parsedSpec).spec; } - const view = new vega.View(vega.parse(vegaSpec), { renderer: 'none' }); + // Specs come from untrusted chat content: block external loads via data.url (loader.load) + // and image mark hrefs emitted into the SVG (loader.sanitize). + const loader = vega.loader(); + loader.load = async () => { + throw new Error('External resource loading is disabled for rendered visualizations'); + }; + const sanitize = loader.sanitize.bind(loader); + loader.sanitize = async (uri: string, options: any) => { + // Resolve with the browser's URL parser so encoding tricks match what it would fetch + const resolved = new URL(uri, document.baseURI); + if (resolved.protocol !== 'data:' && resolved.origin !== location.origin) { + throw new Error('External resource loading is disabled for rendered visualizations'); + } + return sanitize(uri, options); + }; + const view = new vega.View(vega.parse(vegaSpec), { loader, renderer: 'none' }); const svg = await view.toSVG(); return svg; };