itools / svgvalidator
Rejects uploaded SVG files that could run script, load external resources, or hang a renderer: an allowlist validator matching what browsers permit for SVG in an <img> tag
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-09-14 20:23:17 UTC
README
SvgValidator: Reject SVG Uploads That Could Run Script
SvgValidator checks an uploaded SVG file and rejects it if it could run script, load an outside
resource, or hang a renderer. Browsers block the same things when an SVG is shown through an
<img> tag, but not when the file is opened on its own; SvgValidator checks the file, so it is
safe either way. It never rewrites the file.
Why Reject Instead of Clean
A sanitizer strips what it does not like and hands back a changed file, and a logo that renders differently from what the designer exported is a support ticket nobody can explain. A rejection with a reason costs a re-export; a cleaned file that still holds something the filter did not know about costs an XSS. Every rule here is an allowlist, so anything new is rejected until someone adds it.
Quick Start
Requires PHP 8.1+ with ext-xmlreader and ext-libxml (both enabled by default).
composer require itools/svgvalidator
use Itools\SvgValidator\SvgValidator; // upload.php: check the file, then store the original bytes $result = SvgValidator::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 with the headers browsers need header('Content-Type: image/svg+xml'); header('X-Content-Type-Options: nosniff'); // never guess another type from the contents header('Content-Security-Policy: sandbox'); // no script and no cookies, even if a rule is ever bypassed readfile('uploads/logo.svg');
If Apache or nginx serves the upload folder directly, set the same two extra headers there for
.svg files. The rest of the API:
$result->ok; // true when nothing was found $result->errors; // Violation[], one per distinct problem, in file order $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 SvgValidator::checkString($svg); // the same check on SVG source in a string SvgValidator::rules(); // the allowlists, for reading; the rules have no options
What It Blocks
- Script and event handlers.
<script>, everyon*attribute,javascript:URLs and<foreignObject>: a browser runs them the moment the file is opened directly. - Anything that loads from outside the file. Every
hrefandurl()must point at#idor an embeddeddata:image or font: an outside load tells another server who opened the file, and Chrome refuses it in<img>mode anyway. - DTD entity declarations. An entity can expand to markup no other rule sees, or to gigabytes of text.
- Reference loops and expansion bombs. A few dozen
<use>or pattern references can render billions of elements; browsers cap that, server-side rasterizers hang. - XML an HTML parser reads differently. A
<!-->comment, or CDATA holding>inside<title>or<desc>, ends early in an HTML parser, and what follows is live markup if the file is ever served astext/html.
What It Does Not Check
- File size. The file is streamed, so a huge upload passes in a few MB of memory. Cap the size at upload time.
- Extension and MIME type. Only the bytes are read. Refuse
.svgz,.htmland.xmluploads yourself. - Whether the picture is sensible. Blank, enormous, offensive, or another site's logo all pass.
- Renderer bugs. An accepted file is still parsed by libxml2, a browser, or ImageMagick. Keep the rasterizer patched.
- SVG pasted inline into HTML. That makes the markup part of the page. Sanitize on output with DOMPurify instead.
- Files served with the wrong headers. Served as
text/html, or with the type left for the browser to guess, any SVG is a page. Send the headers in the quick start.
When You Might Not Want SvgValidator
- 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 SvgValidator.
- You put SVG source inline in HTML pages. That is a different threat model: the page's origin and the page's scripts. Sanitize on output with DOMPurify.
A check costs less than receiving the upload did; the measurements are in benchmarks/results.md. The rules are tested against real exports from Illustrator, Inkscape, Figma, Affinity Designer, Sketch and CorelDRAW.
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
- Changelog
Related Libraries
- 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