Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Svg Sanitize Laravel Package

enshrined/svg-sanitize

PHP SVG sanitizer based on DOMPurify that cleans untrusted SVG/XML, strips dangerous tags and remote references, supports custom whitelists, minification, and reports sanitization issues.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require enshrined/svg-sanitize

Verify ext-dom and ext-libxml are available (php -m). The package is pure PHP with no framework coupling, so it drops into any Laravel 9/10/11/12 app without configuration.

First use case — sanitize an uploaded SVG before it hits disk

use enshrined\svgSanitize\Sanitizer;

$sanitizer = new Sanitizer();
$clean = $sanitizer->sanitize($request->file('logo')->get());

if ($clean === false) {
    logger()->warning('SVG rejected', ['issues' => $sanitizer->getXmlIssues()]);
    abort(422, 'Invalid SVG');
}

Storage::disk('public')->put('logos/logo.svg', $clean);

Where to look first

  1. The README’s basic example — the whole public API is essentially sanitize(), getXmlIssues(), removeRemoteReferences(), minify(), setAllowedTags(), setAllowedAttrs().
  2. The data/ namespace (TagInterface, AttributeInterface, plus the built-in Tag/ Attribute allowlists) — this is your extension point and the definitive answer to “what does it actually strip?”
  3. The package’s own PHPUnit fixtures (vendor/bin/phpunit) — steal them as your malicious-SVG test corpus.
  4. The hosted demo (http://svg.enshrined.co.uk/) for eyeballing what gets removed.

Pin a floor version. Anything < 1.0.0 has known deficiencies: quadratic sanitize time on element-dense SVGs, href cleaning skipped when href and xlink:href coexist, and case-sensitive xlink:Href namespace lookups. Require ^1.0 and get all three fixes plus PHP 8.5 coverage.

Local triage loop: the bundled CLI scanner is the fastest way to iterate on a failing file without wiring up a route:

php vendor/enshrined/svg-sanitize/svg-scanner.php tests/fixtures/nasty.svg

Implementation Patterns

Bind as a singleton, but don’t let issues leak between requests

// AppServiceProvider::register()
$this->app->singleton(Sanitizer::class, fn () => tap(new Sanitizer(), function ($s) {
    $s->removeRemoteReferences(true);
    $s->minify(app()->isProduction());
}));

This is fine for stateless CLI/queue workers. Under Octane/Swoole the container persists across requests — verify whether getXmlIssues() and internal libxml state reset on each sanitize() call in your installed version, and if in doubt resolve a fresh instance per file (app()->forgetInstance(Sanitizer::class) or new Sanitizer()). Cheap insurance against cross-tenant data appearing in a log line.

Wrap it in a dedicated service, not a controller one-liner

final class SvgSanitizer
{
    public function __construct(private Sanitizer $sanitizer) {}

    /** @throws SvgRejected */
    public function clean(string $svg, array $context = []): string
    {
        $clean = $this->sanitizer->sanitize($svg);

        if ($clean === false) {
            report(new SvgRejected($this->sanitizer->getXmlIssues(), $context));
            throw new SvgRejected(...);
        }

        return $clean;
    }
}

Everything downstream (controllers, jobs, artisan commands, Livewire uploads) talks to SvgSanitizer::clean(); the package stays an implementation detail you can swap.

Custom validation rule for the upload path

final class SafeSvg implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! $value instanceof UploadedFile || $value->getMimeType() !== 'image/svg+xml') {
            $fail('The :attribute must be an SVG.');
            return;
        }

        $sanitizer = new Sanitizer();
        if ($sanitizer->sanitize($value->get()) === false) {
            $fail('The :attribute is not valid SVG XML: '
                . collect($sanitizer->getXmlIssues())->pluck('message')->implode('; '));
        }
    }
}

MIME sniffing alone is spoofable — the parse attempt is the real gate.

Sanitize in the pipeline, then persist the clean copy only

$clean = $this->sanitizer->clean($file->get(), ['user' => $user->id]);

Storage::disk('public')->put($path, $clean);

Never write the original to a web-reachable disk, even temporarily, and never put() from $request->file()->getRealPath() before sanitizing.

Per-tenant / per-feature allowlists

Implement TagInterface / AttributeInterface and pass class names to setAllowedTags() / setAllowedAttrs(). Typical motive: keep the defaults but reject remote xlink:href values while still allowing fragment references (#icon-arrow) used by icon sprites. Because the interface returns a flat array of names, value-level filtering (e.g. “href only if it starts with #”) is not expressible there — combine removeRemoteReferences(true) with a post-pass DOM walk if you need that precision.

Remote references for multi-tenant / SSRF-sensitive apps

$sanitizer->removeRemoteReferences(true) strips attributes pointing at external files. Budget the overhead (~15% in practice) and turn it on by default anywhere user SVGs are rendered in a browser or an email client.

Minify in production, not in development

minify(true) shrinks storage and bandwidth but makes diffing sanitized output against input painful during debugging. Gate it on app()->isProduction().

Queue anything bulk

Sanitization is CPU-bound. For batch imports or migrations, push per-file jobs to a queue rather than looping in a request. If you’re processing the same asset repeatedly (thumbnails, previews), cache by content hash of the pre-sanitized payload — sanitization is deterministic.

Testing you should actually write

Fixture-driven PHPUnit tests using the package’s own fixtures plus your own: <script>, onload=, javascript: in href, <foreignObject>, <use href="https://evil">, DOCTYPE/entity payloads, and a well-formed but invisible SVG. Assert both the rejection path (false + issues logged) and that a clean SVG round-trips byte-for-byte (minus minification).

Gotchas and Tips

Failures are false, not exceptions. No exception hierarchy, no events, no logging hooks. Every call site must branch on false and you must wire up report()/Log::warning() yourself — otherwise rejections are invisible.

XML parse errors precede sanitization. libxml_use_internal_errors(true) and capture libxml_get_errors() before calling sanitize() if you want human-readable diagnostics (“unclosed tag at line 4”); getXmlIssues() reports sanitization decisions, not the low-level parser complaint. Also call libxml_clear_errors() afterwards so errors don’t bleed into unrelated code in the same process.

“Clean” ≠ “renders.” The sanitizer removes XSS vectors; it does not validate that <rect width="0"> produces anything visible, nor that the viewBox is sane. Visual/rendering checks belong in staging.

Silent over-stripping looks like success. If your allowlist is wrong, you get back a valid <svg> root with nothing in it — a truthy string. Add a sanity assertion: after sanitizing a known-good sample, assert child nodes still exist.

Version-sensitive attribute handling. Pre-0.22, xlink:Href (any uppercase) silently failed namespace lookup and skipped sanitization; 1.0.0 additionally fixed href cleaning being skipped when both href and xlink:href are present. If you’re auditing legacy rows in your database, re-run them through the current sanitizer — the old output may be unsafe.

GPL-2.0 is the headline risk, not the code. This is a copyleft license. For a proprietary SaaS it’s usually tolerable (you’re not distributing), but for on-prem/desktop/shipped software it likely is not. Get legal sign-off before it’s woven through five services; the alternative (reimplementing DOMPurify’s SVG rules) is a real cost, and the package is the de-facto PHP standard — used in WordPress’s Safe SVG, Drupal, and TYPO3 core.

Don’t rely on the file extension or MIME alone. Sanitize, then store with a forced .svg name, serve via a controller with Content-Type: image/svg+xml and Content-Disposition, ideally from a separate origin. Sanitization removes scripts; it doesn’t prevent an SVG from leaking referrer/existence via CSS and external URLs — which is exactly why removeRemoteReferences(true) matters.

Reuse or recreate? Both have a cost. A singleton avoids repeated construction; a fresh instance guarantees no state bleed. Decide deliberately and document it — the ambiguity is where bugs land.

The allowlist is the source of truth, not the README. The README doesn’t enumerate what’s permitted. Read data/Attribute.php and data/Tag.php in vendor/ before promising a stakeholder “we block X.” And when you trim the list, remember presentation attributes (viewBox, fill, d) are load-bearing — removing them breaks every icon on the site.

Maintenance posture. Release cadence is slow and the last release metadata in your tooling looks suspicious (a future-dated entry). That’s fine while 1.x keeps working, but treat it as an owned dependency: keep your SvgSanitizer wrapper thin, keep the malicious-fixture test suite in your repo, and track upstream issues yourself rather than expecting a CVE response SLA.

Debugging escape hatch. When a file is rejected and the issues array is unhelpful, load it into the CLI scanner or the hosted demo — the diff between input and output is usually faster to read than any log line.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums