itools / svgsecuritycheck
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
Requires
- php: ^8.1
- ext-libxml: *
- ext-xmlreader: *
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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, everyon*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 astext/html. - Outside resources. Every
hrefmust be a same-file reference (#id), or an embeddeddata:image on<image>and<feImage>(a filter step that draws an image). Everyurl()must be#id, or an embedded font in CSS. CSS functions and at-rules not on a list, such asimage-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
.svgname 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 namelogo.htmlis served as a web page. Refuse.svgzuploads, 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