Search by

itools / svgsecuritycheck

itools

Allowlist check for uploaded SVG files: rejects any that could run script, load an outside resource, or hang a renderer, even when opened outside an <img> tag, and never rewrites a file it accepts

v1.0.0 2026-10-14 05:02 UTC

This package is auto-updated.

Last update: 2026-10-14 05:06:52 UTC


README

SvgSecurityCheck: Reject SVG Uploads That Could Run Script

SvgSecurityCheck checks an uploaded SVG file and rejects it if it could run script, load an outside resource, or hang a renderer. Chrome's <img> mode turns off script and outside loads only while the file is shown through <img>. These rules check the file itself against allowlists, so an accepted file runs no script and loads nothing from another server, even when its URL is opened directly or it is served as text/html. The file is never rewritten: you store the bytes that were uploaded, or tell the uploader why the file was rejected.

Why Reject Instead of Clean

A sanitizer returns a changed file, and a logo that renders differently from what the designer exported is a support ticket nobody can explain. SvgSecurityCheck never rewrites the file: a rejection with a reason costs a re-export, and a cleaned file that still holds something the sanitizer did not know about costs an XSS. Elements, attributes, namespaces, URL forms and CSS functions are all checked against allowlists, so a new one is rejected until it is added.

Documentation

Full docs (browse on GitHub):

  • Error codes and fixes - every message, and what to change in the file or the design tool
  • AI reference - the complete API and every rule in one file, written for AI coding assistants
  • Benchmarks - check times for files from 1 KB to 50 MB, hostile files and real files from public icon sets, test suites and Wikimedia Commons, next to enshrined/svg-sanitize
  • Changelog

Quick Start

Requires PHP 8.1+ with ext-xmlreader and ext-libxml (both enabled by default).

composer require itools/svgsecuritycheck
use Itools\SvgSecurityCheck\SvgSecurityCheck;

// upload.php: check the file, then store the original bytes
$result = SvgSecurityCheck::checkFile($_FILES['logo']['tmp_name']);   // nothing throws: a missing path is a rejection too
if (!$result->ok) {
    foreach ($result->errors as $violation) {
        echo htmlspecialchars($violation->message), "<br>";           // <script> is not allowed in uploaded SVGs
    }
    exit;
}
move_uploaded_file($_FILES['logo']['tmp_name'], 'uploads/logo.svg');

// logo.php: serve it as an image
header('Content-Type: image/svg+xml');
readfile('uploads/logo.svg');

The rest of the API:

$result->ok;                           // true when nothing was found
$result->errors;                       // Violation[], one per distinct problem, in the order found
$violation->code;                      // 'element-not-allowed', stable across releases, so switch on it
$violation->detail;                    // 'script', text from the file, so encode it before output
$violation->message;                   // '<script> is not allowed in uploaded SVGs'
$violation->template;                  // '<%s> is not allowed in uploaded SVGs', for translation with Violation::TEMPLATES
SvgSecurityCheck::checkString($svg);   // the same check on SVG source in a string
SvgSecurityCheck::rules();             // the allowlists as arrays; no option adds to them
SvgSecurityCheck::$maxErrors = 100;    // the size limits are public static properties

What It Blocks

  • Script. The <script> element, every on* attribute, javascript: URLs and <foreignObject> (which holds HTML inside the SVG) are rejected, since a browser runs them when the file is opened directly. XML that an HTML parser reads differently is rejected too, so no script runs even if the file is served as text/html.
  • Outside resources. Every href must be a same-file reference (#id), or an embedded data: image on <image> and <feImage> (a filter step that draws an image). Every url() must be #id, or an embedded font in CSS. CSS functions and at-rules not on a list, such as image-set() and @import, are rejected, and so is <?xml-stylesheet?>. An outside load lets another server log who opened the file.
  • Parser and renderer hangs. DTD entity declarations are rejected: an entity can expand to markup no other rule sees, or to gigabytes of text. So are reference loops, and chains of <use> (which draws a copy of another element) or <pattern> where a few dozen references render billions of elements, which browsers cap and server-side rasterizers do not. <marker> is rejected too: a browser draws its content again at every point of a line, so a short file can keep the tab busy for minutes.

What It Does Not Check

  • File size. The file is streamed, so memory does not grow with its size, except for an embedded data: image, which is held whole while it is checked. Cap the size at upload time.
  • File name and MIME type. Only the bytes are read. Save an accepted file under a .svg name your code picks, as the Quick Start does: a server sets the content type from the extension, so a file kept under the visitor's name logo.html is served as a web page. Refuse .svgz uploads, which browsers render only when the server sends them gzip-encoded.
  • Renderer bugs. An accepted file is still parsed by libxml2, a browser, or ImageMagick. Keep the rasterizer patched.
  • References set in CSS. A <style> rule such as .a { fill: url(#p) } is not counted, so a <pattern> chain built from class rules is accepted. Give server-side rasterizing a time limit.

Additional Protection

An accepted file is safe to serve with only Content-Type: image/svg+xml. Two more headers add a second layer in case a rule is ever bypassed:

header('X-Content-Type-Options: nosniff');    // never guess another type from the contents
header('Content-Security-Policy: sandbox');   // no script and no cookies

If Apache or nginx serves the upload folder directly, set them there for .svg files.

When You Might Not Want SvgSecurityCheck

  • You must accept SVG from the public and cannot ask for a re-export. Use a sanitizer such as enshrined/svg-sanitize and re-check its output with SvgSecurityCheck.
  • You paste SVG source into your HTML pages. Markup written straight into a page, instead of loaded through <img>, becomes part of that page: its <style> rules apply to the whole page and its ids mix with the page's own, which these rules do not check for. Sanitize inline SVG on output with DOMPurify.

Security

Because every list is an allowlist, what the rules cannot anticipate is a browser changing how it handles something already allowed. Update when a new release comes out, and report a file that gets through as described in SECURITY.md.

Related Libraries

  • HtmlSecurityCheck - checks rich-text HTML for anything that could run script and lists each problem it finds.
  • ZenDB - injection-proof PHP/MySQL database layer with automatic XSS-safe output.
  • SmartArray - database rows as chainable collections, with fields that HTML-encode themselves on output.
  • SmartString - PHP strings that HTML-encode themselves on echo, interpolation, and concatenation.

Questions?

This library was developed for CMS Builder. Post a message in our "CMS Builder" forum here: https://www.interactivetools.com/forum/

License

MIT