fix

An SVG Is a Document Wearing an Image Extension

Gareth Heyes tested eight popular SVG sanitizers and bypassed six of them. The techniques are a tour of everything that makes SVG a bad format to accept from strangers.

SVG is XML that browsers treat as a document. It can contain scripts, event handlers, links, animations, embedded HTML, and — through XSLT — arbitrary transformations. Applications that accept SVG uploads usually run them through a sanitizer first. The sanitizer's job is to enumerate what's dangerous and remove it. The browser's job, from the attacker's perspective, is to keep adding features that compose into execution.

Three families of bypass

Namespace and case confusion. You don't need <foreignObject> to get HTML into an SVG. Switch the XML namespace on one element to XHTML and the browser renders it as HTML inside the SVG — so a <frame src="javascript:alert(document.domain)"> just works. Same with <iframe srcdoc="...">.

A subtler one: the browser converts <foreignobject> to camelCase foreignObject when parsing, but a sanitizer that later searches with querySelectorAll('foreignobject') finds nothing. The element is there; the selector isn't. A third: a nested <svg onload="alert(1)"/> goes straight through.

None of these are clever cryptography. They're mismatches between the parser's model and the sanitizer's model — the exact place where filters fail.

Animation. One library let <animate> through, so you can animate a link's href into a javascript: URL after load. Another bypass uses <animateTransform> to set a JavaScript URL in Safari, a technique credited to Kevin Mizu.

The general shape: a sanitizer checks the static attribute value, and the browser changes it later. Any sanitizer that validates once, before the document can animate, is checking a value that won't be the value.

XSLT and data URLs. This is the best one. An xml-stylesheet processing instruction can point at a data URL containing an XSLT stylesheet, with no separate file needed. The stylesheet matches the document root, outputs HTML, and uses xsl:element name="{concat('scr','ipt')}" to build a script tag — so the string "script" never appears literally in the payload. It executes in the document's origin.

Heyes notes the feature is deprecated and due for removal in November 2026. That's a good reminder that deprecation is not a security control: until it's gone, it's a bypass, and after it's gone, the next feature is the bypass.

The header trick

The finding I'd file under "sanitizers don't model the whole response": Firefox lets you load external CSS via a Link response header, and data URLs work there too. Heyes worked out that the same applies to XSLT.

So you send a response whose body is harmless XML, with a Link: <data:application/xml,...>; rel="stylesheet"; type="text/xsl" header. The stylesheet runs, copies an <img onerror> into a <foreignObject>, and the JavaScript fires. The body a sanitizer inspected is clean. The document the browser built is not.

That's the deeper lesson: a sanitizer that only sees the body is sanitizing one of the inputs. Response headers, content type, and the embedding context are part of the document too.

Safari's image import

Safari allows <use href="..."> to import same-origin files, and it goes further than Firefox: it will import an image file as SVG. Upload a GIF containing the XML payload; it won't render when opened directly, but <use href="/svg/test.gif#x"> imports it and executes the handler inside.

An image upload path that validates extensions or MIME types, and a sanitizer that only looks at .svg files, is defeated by a file the pipeline thought was a GIF.

What held

DOMPurify and darylldoyle/svg-sanitizer survived everything in the post. That's worth noting: allowlist-based sanitizers that parse with the browser's own parser, rather than pattern-matching strings, are the ones that hold up. The failures cluster in libraries that enumerate bad things.

My take

The right conclusion isn't "use DOMPurify" — although you should. It's that sanitizing SVG perfectly is an unbounded commitment. You're tracking a document format that the browser keeps extending, and every new feature is a potential composition.

The architectural answer is to stop treating sanitization as the only control: serve user-supplied SVG from a separate origin with no cookies, no ambient authority, and a restrictive content security policy, so that even a successful injection has nothing to steal. Sanitize as defense in depth, not as the boundary.

Heyes's post is a catalogue of why. Eight libraries, six bypasses, and two unnamed applications he's still disclosing.

Source: Scalable Vector Grief — Gareth Heyes, PortSwigger Research.

#security#web#svg#xss