Published: Tuesday, 27 October 2026 at 14:29 UTC
Updated: Tuesday, 27 October 2026 at 14:29 UTC

I tested eight popular SVG sanitizers and bypassed six of them. In this post, I’ll show you how a response header can run JavaScript in Firefox even when the response body is completely harmless XML. I’ll also show you how to get Safari to import a GIF as SVG and execute JavaScript from it.
Hacking SVG sanitizers
The targets
SVG is an image format that can contain scripts, event handlers, links, animations and even embedded HTML, so applications that accept SVG uploads usually run them through a sanitizer first. To see how well those sanitizers hold up, I tested the most popular SVG libraries according to ChatGPT:
Warming up: namespace confusion
Library affected: mattkrick/sanitize-svg
This library has so many bypasses that I’ll only cover the interesting ones.
My first finding was that you don’t need to get HTML into an SVG. Switch the XML namespace on a single element to XHTML and the browser renders it as HTML directly inside the SVG. A with a JavaScript URL is all it takes:
The same namespace trick works with iframes, which gives you srcdoc to play with:
The library could also be bypassed with a plain old . It renders the SVG, which converts the element name to its camel-case form, foreignObject. It then hunts for the element with querySelectorAll('foreignobject'), which finds nothing, so the payload goes straight through:
Even a nested SVG with an onload handler got through:
Animating your way to a JavaScript URL
Library affected: Borewit/svg-sanitizer
This one didn’t take much effort. The library let tags through untouched, so you could animate a link’s href into a JavaScript URL:
I bypassed the same library a second way using
Kevin Mizu’s
trick
, which uses
XSS from a self-contained XSLT stylesheet
Libraries affected: bgalek/safe-svg, mattkrick/sanitize-svg, alnorris/SVG-Sanitizer
Now things get more interesting. This is a pretty epic technique: you can get JavaScript execution from an XSLT stylesheet without needing a separate file. I was desperate to blog this one, because unfortunately the feature is deprecated and due for removal November 2026.
The trick is to point an xml-stylesheet processing instruction at a data URL. Base64 is the most elegant way to encode the stylesheet, but URL encoding works too:
Here’s the stylesheet that gets base64 encoded:
It matches the root of the document and outputs an HTML page containing a script that alerts document.domain, proving the code runs in the document’s origin rather than a null origin. Put it all together and you get this:
Turning a response header into XSS
In the back of my mind was a trick
Paulos Yibelo
once told me about: Firefox lets you load external CSS files using a Link response header. I figured out that data URLs work there too, which got me thinking: if it works for CSS, could it work for XSLT too? You’re damn right it can.
First, I needed a version of the stylesheet that fits in a URL. This one has two layers. The outer stylesheet is URL encoded into a data URL. Inside it, XSLT’s document() function loads a second data URL, URL encoded again, containing an XHTML with an onerror handler. The stylesheet copies that into a , where it fires.
As a processing instruction, it looks like this:
Here’s the payload before encoding. I’ve left the Hackvertor tags in so you can see where each layer of URL encoding is applied:
<@encode(url)>
With the stylesheet ready, all that’s left is to move it out of the document and into a Link header:
To end this piece of research, here’s another epic behaviour, this time in Safari. Firefox and Safari both let you use the
Alas, you can’t make a true SVG/image polyglot because SVG is a little too strict. However, if you can upload a GIF that contains the payload, it won’t render when you open it in the browser, but it can still be imported with a element!
I hope you enjoyed this post. I also exploited two popular applications that I can’t name yet, but I’ll update the post once I can. Both DOMPurify and darylldoyle/svg-sanitizer held up against everything in this post, which is quite impressive.

