mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
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 <image href> 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 <Zureno@users.noreply.github.com> * 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 <Zureno@users.noreply.github.com>
This commit is contained in:
+16
-1
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user