PHPackages                             abdian/laravel-upload-guard - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. abdian/laravel-upload-guard

ActiveLibrary[File &amp; Storage](/categories/file-storage)

abdian/laravel-upload-guard
===========================

Secure file upload validation for Laravel — fail-closed scanning for polyglot web shells, malicious PDFs/SVGs, zip bombs, Office macros, and spoofed MIME types.

v1.0.0(today)00MITPHPPHP ^8.1CI failing

Since Jun 20Pushed todayCompare

[ Source](https://github.com/abdian/laravel-upload-guard)[ Packagist](https://packagist.org/packages/abdian/laravel-upload-guard)[ Docs](https://github.com/abdian/laravel-upload-guard)[ RSS](/packages/abdian-laravel-upload-guard/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (8)Versions (2)Used By (0)

🛡️ Laravel Upload Guard
=======================

[](#️-laravel-upload-guard)

**Fail-closed file-upload validation for Laravel.**

A defense-in-depth layer that detects and blocks common malicious uploads — polyglot web shells, malicious PDFs &amp; SVGs, zip bombs, Office macros, and spoofed MIME types — using structural parsing and content sanitization, not just regex.

*Not an antivirus, and not a sole security boundary — see [Limitations](#limitations--not-a-security-boundary).*

[![Latest Version](https://camo.githubusercontent.com/7776b502dd325844e74f5aeeb8177cafd8a96337c48d8bedbeee569132caad54/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61626469616e2f6c61726176656c2d75706c6f61642d67756172642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/abdian/laravel-upload-guard)[![Total Downloads](https://camo.githubusercontent.com/83e3c2ee752131e474a947d7a32c4836c04975715c371db2431638ce5bbbbd50/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61626469616e2f6c61726176656c2d75706c6f61642d67756172642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/abdian/laravel-upload-guard)[![Tests](https://camo.githubusercontent.com/a9af012c14b7fb7895bac36f9239721c2bf9d8e61b569f28c3d443996c716c76/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61626469616e2f6c61726176656c2d75706c6f61642d67756172642f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/abdian/laravel-upload-guard/actions/workflows/tests.yml)[![PHP Version](https://camo.githubusercontent.com/cbe68d20542ceec8729e6c5b862d273540da6144162bbe79b2239e5c839f2b00/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f61626469616e2f6c61726176656c2d75706c6f61642d67756172642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/abdian/laravel-upload-guard)[![License](https://camo.githubusercontent.com/67fa8c64520c50c45b8711f6dab6fb1aa19f0f8ea27e182d045f7420be2e1f62/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f61626469616e2f6c61726176656c2d75706c6f61642d67756172642e7376673f7374796c653d666c61742d737175617265)](LICENSE)

```
// One rule. Fail-closed by default.
$request->validate([
    'file' => 'required|safeguard',
]);
```

---

Why?
----

[](#why)

Laravel's built-in `mimes` / `mimetypes` rules trust the **client-declared** type and a coarse extension map. An attacker can upload `shell.php` renamed to `avatar.jpg`, a real JPEG with PHP appended after the image data (a *polyglot*web shell), an SVG carrying ``, a PDF with an auto-run `/JavaScript`action, or a 42 KB zip that expands to petabytes. None of those are caught by extension checks.

**Upload Guard inspects the actual bytes** — magic structure, decoded PDF/zip streams, sanitized SVG/Office internals — and **blocks anything it cannot prove is safe**.

> ### 🔒 Design principle: *fail closed*
>
> [](#-design-principle-fail-closed)
>
> When the package cannot be sure a file is safe, it **blocks** the upload. Unknown content types, unparsable containers, and scanner exceptions all resolve to **reject** — never to *allow*. Stricter than lax validators by design. It raises the bar a lot, but content scanning is best-effort — pair it with the operational [hardening steps](#limitations--not-a-security-boundary) below.

---

Threat coverage
---------------

[](#threat-coverage)

ThreatHow Upload Guard handles it🐚 **Polyglot web shells** (PHP in JPEG / PDF / ZIP)Always-on code scan on **every** upload, regardless of detected type🎭 **Spoofed MIME / double extension**Structural byte detection + strict extension ↔ content matching🖼️ **Malicious SVG** (XSS / XXE)Allowlist sanitization; DOCTYPE/entity/script stripping; **stored clean**📄 **Malicious PDF** (`/JavaScript`, `/OpenAction`, `/Launch`)Decode-before-scan, indirect-`/Filter` resolution, bounded inflation💣 **Zip bombs &amp; zip-slip****Global** actual-bytes cap across nested archives; traversal / symlink / NTFS-ADS rejection📎 **Office macros + macro-less RCE**OOXML **and** legacy OLE/CFB; VBA, ActiveX, **DDE/DDEAUTO**, remote `attachedTemplate`🧨 **Image decompression bombs**Header pixel/byte cap **before any decode**; optional re-encode to strip payloads🌊 **Upload DoS**Hard size caps + optional per-IP rate limiting + opt-in forensic quarantine---

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Quick start](#quick-start)
- [Usage](#usage)
    - [With Laravel's `mimes` rule](#with-laravels-mimes-rule)
    - [Fluent configuration](#fluent-configuration)
    - [Individual rules](#individual-rules)
- [Fluent API reference](#fluent-api-reference)
- [Configuration](#configuration)
- [How it works](#how-it-works)
- [Limitations &amp; not a security boundary](#limitations--not-a-security-boundary)
- [Testing](#testing)
- [Security](#security)
- [License](#license)

---

Installation
------------

[](#installation)

```
composer require abdian/laravel-upload-guard
```

The service provider is auto-discovered. Publish the (fully commented) config to tune behavior:

```
php artisan vendor:publish --tag=safeguard-config
```

### Requirements

[](#requirements)

**PHP**8.1 · 8.2 · 8.3 · 8.4 · 8.5**Laravel**10 · 11 · 12 · 13**Required extensions**`fileinfo`, `zip`, `dom`, `libxml`**Optional extensions**`exif` (EXIF inspection/stripping) · `gd` *or* `imagick` (image re-encode mode)> Optional extensions degrade gracefully — the package installs and runs without them.

---

Quick start
-----------

[](#quick-start)

```
public function store(\Illuminate\Http\Request $request)
{
    $request->validate([
        'file' => 'required|safeguard',
    ]);

    $request->file('file')->store('uploads');
}
```

The single `safeguard` rule runs — **by default, no fluent calls required**:

✅ structural MIME detection + dangerous-type blocking · ✅ strict extension/content matching · ✅ always-on code scanning · ✅ SVG sanitization · ✅ image &amp; PDF scanning · ✅ archive **and** Office-macro scanning.

---

Usage
-----

[](#usage)

### With Laravel's `mimes` rule

[](#with-laravels-mimes-rule)

```
$request->validate([
    'file' => 'required|safeguard|mimes:jpg,png,pdf',
]);
```

`safeguard` reads the allowed extensions and enforces that the file's **real**content type matches them.

### Fluent configuration

[](#fluent-configuration)

```
use Abdian\UploadGuard\Rules\Safeguard;

$request->validate([
    'avatar' => ['required', (new Safeguard)
        ->imagesOnly()
        ->maxDimensions(1920, 1080)
        ->blockGps()
        ->stripMetadata(),
    ],

    'document' => ['required', (new Safeguard)
        ->pdfsOnly()
        ->maxPages(50)
        ->blockJavaScript()
        ->blockExternalLinks(),
    ],

    'report' => ['required', (new Safeguard)
        ->documentsOnly(),   // archive + macro scanning are already on by default
    ],
]);
```

### Individual rules

[](#individual-rules)

Compose only the scanners you need:

```
$request->validate([
    'avatar'   => 'required|safeguard_mime:image/jpeg,image/png|safeguard_image',
    'icon'     => 'required|safeguard_svg',
    'document' => 'required|safeguard_pdf|safeguard_pages:1,10',
    'photo'    => 'required|safeguard_dimensions:100,100,4000,4000',
    'archive'  => 'required|safeguard_archive',
    'report'   => 'required|safeguard_office',
]);
```

RuleDescription`safeguard`All-in-one, fail-closed pipeline`safeguard_mime:type1,type2`Real content-type allowlist (+ dangerous-type block)`safeguard_php`Always-on PHP/script code scan`safeguard_svg`Allowlist SVG sanitization`safeguard_image`Image bomb / metadata / byte / trailing-data scan`safeguard_pdf`Decode-before-scan PDF analysis`safeguard_archive`Streaming archive inspection (zip/tar/gz)`safeguard_office`OOXML + legacy OLE macro / DDE / template detection`safeguard_dimensions:maxW,maxH,minW,minH`Image dimension limits`safeguard_pages:min,max`PDF page-count limits> **Note on `safeguard_archive` string params:** parameters are added to the **block** list (e.g. `safeguard_archive:iso,bin` also blocks `.iso`/`.bin`). To *allow* an otherwise-blocked extension, use the fluent rule: `(new SafeguardArchive)->allow(['sh'])`.

---

Fluent API reference
--------------------

[](#fluent-api-reference)

All methods on `Abdian\UploadGuard\Rules\Safeguard` return `$this` (chainable).

MethodEffect`allowedMimes(array $mimes)`Restrict to a real-content-type allowlist (`'image/*'` wildcards supported)`imagesOnly()` / `pdfsOnly()` / `documentsOnly()` / `archivesOnly()`Restrict to a file family`maxDimensions(int $w, int $h)` / `minDimensions(int $w, int $h)`Image dimension bounds`dimensions(int $minW, int $minH, int $maxW, int $maxH)`All four bounds at once`maxPages(int)` / `minPages(int)` / `pages(int $min, int $max)`PDF page-count bounds`blockGps()`Reject images that contain GPS/EXIF location data`stripMetadata()`Strip metadata from images`blockJavaScript()`Reject PDFs containing JavaScript`blockExternalLinks()`Reject PDFs containing external links`strictExtensionMatching(bool = true)`Force/disable extension ↔ content matching`scanArchives(bool = true)`Toggle archive scanning (on by default)`blockMacros(bool = true)` / `allowMacros()`Toggle Office-macro blocking (on by default)---

Configuration
-------------

[](#configuration)

The published `config/safeguard.php` is fully commented; highlights:

```
'max_scan_size'   => 25 * 1024 * 1024, // files larger than this are rejected
'over_cap_policy' => 'reject',         // or 'header_only'

'mime_validation' => [
    'strict_check'       => true,
    'block_dangerous'    => true,
    'block_undetectable' => false,     // set true to reject unknown content types
],

'archive_scanning' => [
    'enabled'               => true,                 // ON by default
    'max_decompressed_size' => 500 * 1024 * 1024,    // hard cap on ACTUAL bytes (global)
    'max_files_count'       => 10000,
    'max_nesting_depth'     => 3,
],

'office_scanning' => [
    'enabled'       => true,           // ON by default
    'block_macros'  => true,
    'block_activex' => true,
],

'svg_scanning'   => ['mode' => 'sanitize'],                 // or 'reject'
'image_scanning' => ['max_pixels' => 64_000_000, 'reencode' => false],

'rate_limiting'  => ['enabled' => false],  // DoS guard (opt-in)
'quarantine'     => ['enabled' => false],  // forensic quarantine (opt-in)
```

Every key is also overridable via environment variables (e.g. `SAFEGUARD_ARCHIVE_SCAN`, `SAFEGUARD_SVG_MODE`, `SAFEGUARD_IMAGE_REENCODE`).

---

How it works
------------

[](#how-it-works)

**Always-on code scanning**Every upload is scanned for PHP/script openers (`
