Scalable Vector Grief
I conducted tests on eight commonly used SVG sanitizers and successfully evaded six of them. This report will detail how a response header can execute JavaScript within Firefox, even when the response body consists solely of harmless XML. Additionally, I will demonstrate a method to make Safari import a GIF as SVG and subsequently run JavaScript from it.
SVG files can incorporate various elements, such as scripts, event handlers, links, animations, and embedded HTML. Consequently, applications that permit SVG file uploads often execute them through sanitizers prior to processing. To assess the efficacy of these sanitizers, I examined the most widely utilized SVG libraries, as indicated by ChatGPT.
The initial discovery was that there is no necessity for an external fragment (foreignObject) to incorporate HTML into an SVG. By switching the XML namespace on a single element to XHTML, the browser renders it as HTML directly within the SVG. A frame containing a JavaScript URL is sufficient to bypass the sanitizer: The identical namespace trick applies to iframes, enabling the utilization of srcdoc to manipulate the content.
Moreover, the library could be bypassed using a conventional foreignObject element. The browser converts the element name to its camel-case form, rendering it as foreignObject. It subsequently searches for the element using querySelectorAll("foreignobject"), which yields no results, allowing the payload to pass through unaltered: Even a nested SVG with an onload handler successfully evaded the sanitization.
This process was relatively straightforward. The library permitted animate tags to pass through without modification, enabling the transformation of a link's href into a JavaScript URL: Another method to bypass the same library was discovered using Kevin Mizu's technique, which leverages animateTransform to assign a JavaScript URL in Safari: The libraries that were affected by these bypasses include bgalek/safe-svg, mattkrick/sanitize-svg, and alnorris/SVG-Sanitizer.
The situation becomes more intriguing with the following technique. It is possible to execute JavaScript from an XSLT stylesheet without the need for a separate file. This is a particularly noteworthy technique, as it is scheduled for removal in November 2026 due to its deprecated nature. The approach involves pointing an xml-stylesheet processing instruction at a data URL.
Encoding the stylesheet in base64 or URL encoding are the most elegant methods. Here is the stylesheet encoded in base64: I base64 encoded the following stylesheet: Upon execution, the stylesheet matches the root of the document and outputs an HTML page containing a script that alerts document.domain, demonstrating that the code runs within the document's origin rather than a null origin.
The complete process is illustrated below: Lastly, I recalled a technique shared by Paulos Yibelo regarding Firefox's ability to load external CSS files using a Link response header. I realized that data URLs could also be utilized in this context, leading me to wonder if this method could be applied to XSLT as well. Indeed, it can.
The stylesheet must be shortened to fit within a URL. The outer stylesheet is encoded into a data URL, within which the XSLT's document() function loads a second data URL, encoded again, containing an XHTML img with an onerror handler. The stylesheet then copies this into a foreignObject, where it triggers the payload. The processing instruction appears as follows: After encoding the stylesheet, the final step involves moving it from the document to a Link header.
The response body remains harmless XML, yet it successfully executes in Firefox. The libraries that were impacted by this bypass include cloudflare/svg-hush, jams2/py-svg-hush, and bgalek/safe-svg. To conclude this investigation, I present another remarkable behavior observed in Safari. Both Firefox and Safari support the use element for referencing same-origin files, but Safari goes a step further by allowing the import of image files as SVG.
However, creating a true SVG/image hybrid is challenging due to SVG's strict nature. Nevertheless, if a GIF containing the payload can be uploaded, it can still be imported using a use element, thereby bypassing all three libraries. I trust you found this post engaging. I also exploited two popular applications that are unmentioned here, but I will update the post once I have more information.
Both DOMPurify and darylldoyle/svg-sanitizer successfully resisted all bypasses detailed in this report, which is quite impressive.
Written by urgent.news from Lobsters's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.