SVG Security Vulnerabilities: Stored XSS, XML Injection & Sanitization Guide

Unlike raster formats (JPEG, PNG, WebP) which store compressed numeric pixel matrices, Scalable Vector Graphics (SVG) is a dialect of Extensible Markup Language (XML) defined by the W3C. An SVG document possesses a full Document Object Model (DOM), supports CSS styling, and can embed interactive scripting environments.
1. The Anatomy of Stored SVG XSS: Beyond Simple <script> Tags
Many naive web applications attempt to secure SVG uploads with simplistic string regex checks: svgContent.includes('<script'). In modern AppSec, regex-based tag blocking is virtually useless against the rich variety of SVG execution contexts:
2. The Dangerous Myth: SVGO Is an Optimizer, NOT a Security Sanitizer
One of the most dangerous fallacies in modern web engineering is using svgo (SVG Optimizer) as an input validation or sanitization filter.
SVGO’s Objective: Eliminate redundant precision digits, combine paths, strip editor metadata (like Adobe Illustrator tags), and reduce file size.
Security Sanitizer Objective: Parse the full XML syntax tree, remove dangerous DOM nodes, strip JavaScript attributes, validate safe URI schemes, and eliminate mutation XSS (mXSS) anomalies.
If an SVG contains obfuscated JavaScript payloads or polyglot payloads inside permissible nodes, SVGO will happily optimize the surrounding coordinates while preserving the malicious executable script. Never rely on minification for security boundaries.
3. Hidden Attack Vectors: <foreignObject>, <animate>, and XML Entities
The SVG specification integrates deeply with XHTML and SMIL (Synchronized Multimedia Integration Language), opening up esoteric attack surfaces:
The <foreignObject> tag allows raw HTML documents to be rendered directly inside an SVG container. Attackers embed form phishing overlays, iframes, or direct <iframe src="javascript:..."> tags inside what appears to be an innocent vector icon.
SMIL animation tags can dynamically mutate element attributes over time. An attacker sets an initially harmless attribute that transitions into href="javascript:..." at runtime, bypassing static string scanners.
Recursive entity declarations (<!ENTITY lol "lol"><!ENTITY lol2 "&lol;&lol;">) can exhaust server RAM within milliseconds when parsed by unhardened server-side image processors (such as older librsvg or libxml2 instances).
4. Defense-in-Depth: CSP, Sandboxing & Isolated Delivery
To guarantee zero vulnerability footprint across customer environments, implement multi-layered defense-in-depth:
| Layer | Implementation Standard | Security Benefit |
|---|---|---|
| Domain Isolation | static-assets-cdn.com | Even if JavaScript executes, it cannot access session cookies or localStorage of app.com. |
| HTTP Headers | Content-Security-Policy: default-src 'none'; | Forces browser to refuse execution of any inline scripts or external fetches. |
| HTML Rendering | <img src="icon.svg" /> | Browser treats graphic as passive pixel frame; scripts disabled automatically. |
5. Production Sanitization Architecture: DOMPurify & Defanging
If your application must display inline SVG markup (to allow dynamic CSS hover states or theming), you must run user-provided vector markup through an audited AST-based sanitizer like DOMPurify with explicit SVG configurations:
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeUserSvg(rawSvgString: string): string {
// 1. Enforce strict SVG-only profile
const clean = DOMPurify.sanitize(rawSvgString, {
USE_PROFILES: { svg: true, svgFilters: true },
ADD_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g'],
FORBID_TAGS: [
'script',
'foreignObject',
'iframe',
'object',
'embed',
'animate',
'set',
'use', // Mitigates external resource inclusion attacks
],
FORBID_ATTR: [
'onload',
'onerror',
'onclick',
'onmouseover',
'href',
'xlink:href',
],
});
return clean;
}6. Secure File Upload & Sanitization Pipeline in TypeScript
In production backend pipelines (Node.js / Next.js API route), sanitize before storing to object storage (e.g., AWS S3):
// app/api/upload/svg/route.ts
import { NextResponse } from 'next/server';
import { sanitizeUserSvg } from '@/lib/security/svgSanitizer';
export async function POST(req: Request) {
const formData = await req.formData();
const file = formData.get('file') as File;
if (!file || file.type !== 'image/svg+xml') {
return NextResponse.json({ error: 'Invalid SVG format' }, { status: 400 });
}
// Read raw XML byte text
const rawSvg = await file.text();
// Run through AST sanitizer
const sanitized = sanitizeUserSvg(rawSvg);
if (!sanitized || sanitized.trim().length === 0) {
return NextResponse.json({ error: 'Malicious payload detected' }, { status: 422 });
}
// Upload sanitized text to S3 with safe headers
// Content-Type: image/svg+xml
// Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline';
return NextResponse.json({ success: true });
}7. Frequently Asked Technical Questions
Can an SVG file execute JavaScript when embedded via an <img> tag?+
However, the vulnerability arises when the user clicks 'Open image in new tab' (navigating to the raw SVG URL), or when the application serves the SVG with 'Content-Type: image/svg+xml' without CSP headers, or renders it inline via dangerouslySetInnerHTML or React SVG loaders. In those contexts, JavaScript executes with full origin privileges.
Why can't I just use SVGO to sanitize user-uploaded SVGs?+
SVGO can easily be configured to leave scripts or interactive elements intact. Furthermore, novel mutation XSS vectors and nested namespace bypasses can easily survive minification passes. True security sanitization requires an AST-aware sanitizer like DOMPurify configured with strict SVG profiles.
What Content-Type headers should be used when serving untrusted SVGs from cloud storage?+
Additionally, serve untrusted files from an isolated, cookie-free sandbox domain (such as user-uploads.example.com instead of app.example.com) so that any theoretical script execution occurs in an origin isolated from user session cookies and local storage tokens.
How do XML External Entity (XXE) attacks work in SVG parsers?+
For example, <!ENTITY xxe SYSTEM "file:///etc/passwd"> references local files. When server-side libraries (such as older ImageMagick or libxml2 builds) parse the SVG, the file contents get leaked. Parsers must explicitly disable external DTD and entity resolution (resolveExternals = false).
Optimize & Clean SVG Vectors Client-Side
Strip editor bloat, minify path coordinates, and inspect vector markup securely in browser memory without sending private files to any server.