laravel-at/laravel-image-sanitize
Laravel middleware that re-encodes uploaded images to strip hidden malicious payloads, reducing the risk of code execution. Requires PHP 8.3+, Laravel 12/13, and Intervention Image 4.
Architecture fit. The package is a deliberately narrow, single-responsibility middleware for Laravel's HTTP kernel: it inspects inbound multipart uploads, pattern-matches binary content for PHP-injection artifacts (<?php, <?=, phar), and re-encodes any hit through Intervention Image 4 before the controller ever sees the file. That is a good architectural shape for us — it is stateless, container-resolved (ImageManager respects an existing app binding), config-driven, and exposed both as middleware and as a facade (ImageSanitize::detect() / ::sanitize()) for use inside jobs or console commands. It does not touch storage, naming, or persistence, so it composes cleanly with our existing upload pipeline rather than competing with it. Constraint: PHP 8.3+, Laravel 12 or 13, Intervention Image 4 — this is a hard gate on our upgrade calendar.
Integration feasibility. Low. composer require, optional vendor:publish --tag=image-sanitize-config, register the image-sanitize alias in bootstrap/app.php, and attach it to upload routes. No migrations, no schema, no queue, no external service, no credentials. Realistic effort is one sprint-day for a first route, plus test-fixture work.
Technical risk. The dominant risk is over-trusting the control. This is a blocklist, and blocklists are bypassable: obfuscated payloads, alternate PHP tags, polyglot files whose real type differs from what finfo reports, and — most importantly — out-of-scope formats. SVG is unsupported by default (correct, it is an XSS vector, but it means SVG uploads receive zero protection here), and non-image vectors like PDF, ZIP, and Office documents that enable phar:// deserialization are entirely outside the package's scope. Second risk is silent coverage gaps: a published config is never updated by the package, and v5.0.1 already required operators to hand-add <?= to their patterns array. Third is synchronous CPU/memory cost — decoding and re-encoding large, animated, or high-resolution images inside the request cycle under the default GD driver. Fourth is maintainer concentration: 345 stars, effectively zero downstream dependents, a two-person credit list, and a history of breaking majors (v1.1.0 removed a config file, v5.0.0 reintroduced one; v3/v4/v5 each changed framework, library, or return-type contracts). Forkable, MIT, small surface — but we are effectively the maintainer of last resort.
Key questions for the vendor / our own review.
allowed_mime_types — does a disallowed MIME type get rejected, or silently skipped and passed through? "Skip" would be a material gap.UploadedFile in the request so downstream validation and storage see sanitized bytes, or does it only validate?strip_metadata: true (default) remove EXIF orientation, ICC profiles, and GPS in ways that affect our rendering fidelity or privacy posture?Stack fit. Ideal fit for a Laravel 12/13 application already using Intervention Image 4 for thumbnailing. If we are on Intervention 3 or Laravel ≤11, we cannot use v5.x at all — we would be pinned to v2.x (Laravel 9–11, Intervention 3), which has no publishable config file, no WebP in defaults, and no nested-upload fixes. That downgrade path is not worth it; align the package adoption with the framework/Intervention upgrade, not before.
Migration path. Two entry scenarios. (a) Greenfield: install v5.0.1 directly, publish config, commit it to the repo, and treat config/image-sanitize.php as application-owned code with a documented upgrade diff procedure. (b) Existing in-house sanitizer or a controller-level check: run the package in parallel behind a feature flag on one low-risk route, compare detection outcomes against our current logic on a replay corpus, then remove the bespoke code. The v5.0.0 breaking changes (encoded-image return type, explicit Response return type, Intervention 4) mean any internal code calling sanitize() must be audited at the same time.
Compatibility and sequencing.
composer.json.patterns beyond defaults (framework-specific markers, <?php variants, __halt_compiler, encoded payload signatures); explicitly decide the SVG and non-image policy outside this package.Content-Disposition: attachment on serving.Maintenance. Low code churn, non-trivial version churn. We must own: the published config file (diff it against package defaults on every upgrade — v5.0.1 is the cautionary example), the composer constraint, and a quarterly review of the upstream repo's activity. Budget a small recurring allowance for major-version upgrades, which have historically arrived roughly annually and carry breaking changes. Add a composer check-equivalent (Pint + PHPStan 7 + tests) to our CI so our forked config and fixtures stay validated.
Support. No commercial support; MIT with a security contact email in the README. Escalation path is a fork. Given the tiny code surface (one middleware, one sanitizer, two lists), we can reasonably vendor or fork it if upstream goes dormant — but we should designate an owner now rather than discover the need during an incident.
Scaling. Sanitization is CPU- and memory-bound and runs inline. Under load, large image uploads become the slowest request path on the box and the most likely source of memory_limit exhaustion (GD is materially more memory-hungry than Imagick on large and animated assets). Mitigations: cap upload size at the edge (nginx client_max_body_size), rate-limit upload endpoints, prefer Imagick if available, and if upload volume grows, shift re-encoding into a queued job while keeping the pattern scan synchronous (the facade supports exactly this split). Horizontal scaling is unaffected — the package holds no state.
Failure modes. (1) Corrupt or truncated image causes an exception in the decode path → 5xx on the upload route; wrap and return a 422 with a distinguishable error code. (2) Memory exhaustion on oversized or animated images → 500s or worker crashes; enforce size ceilings before the middleware runs. (3) False positives on legitimate files whose bytes contain a pattern → gratuitous re-encode, quality/ICC loss; monitor detection rates and treat a spike as a signal. (4) False negatives — the real risk — stored malicious payloads that later become RCE if the storage directory is ever made executable; the storage hardening control is the actual backstop, not this package. (5) Config drift after upgrade silently widening or narrowing coverage. (6) Nested/multi-file uploads: v5.0.0 fixed mixed flat/nested and repeated array field names, so anything below v5.0.0 is a known correctness gap. (7) Double sanitization if an upstream WAF or image proxy already re-encodes — wasted CPU, not a correctness issue.
Ramp-up. A competent Laravel engineer is productive in half a day; the README is clear, current, and honest about limitations. The real ramp-up cost is organizational, not technical: teaching the team that this is defense-in-depth for images only, and that it does not retire the need for upload validation, filename generation, non-executable storage, or a separate story for SVG, PDF, and archive uploads. Frame it as one layer in an upload-security standard, ship it alongside the other layers, and measure detection events — otherwise we will have bought a control we cannot prove is working.
How can I help you explore Laravel packages today?