hassan-lateef / guardian
A Laravel package that provides a deep file security validation layer before files are stored — detecting malicious code, double extensions, MIME spoofing, and embedded payloads.
Requires
- php: ^8.1
- ext-dom: *
- ext-fileinfo: *
- ext-gd: *
- ext-libxml: *
Requires (Dev)
- laravel/pint: *
- orchestra/testbench: ^9.0
- pestphp/pest: *
README
Guardian is a Laravel package for validating uploaded files before you store them.
It is designed to catch common upload abuse cases such as:
- blocked executable extensions like
.php,.sh,.exe - double extensions like
avatar.php.png - MIME spoofing where the file contents do not match the extension
- suspicious payloads embedded inside text files, images, PDFs, or SVGs
- malformed or fake image/PDF/ZIP-based documents
- dangerous SVG elements, attributes, and
javascript:links
What Guardian Currently Does
The active scan pipeline in the package is:
ExtensionScannerMimeScannerContentScannerSvgScannerStructuralScanner
Guardian fails fast. The first scanner that rejects the file stops the pipeline.
Installation
composer require hassan-lateef/guardian
Publish the config:
php artisan vendor:publish --tag=guardian-config
Usage
Middleware
Guardian registers the guardian middleware alias automatically through the package service provider.
use Illuminate\Support\Facades\Route; Route::post('/upload', UploadController::class)->middleware('guardian'); Route::middleware('guardian')->group(function () { Route::post('/avatar', [ProfileController::class, 'updateAvatar']); Route::post('/document', [DocumentController::class, 'store']); });
The middleware scans every uploaded file in the request, including nested file arrays.
Validation Rule
use Hassan\Guardian\Rules\GuardianRule; $request->validate([ 'avatar' => ['required', 'file', new GuardianRule()], 'document' => ['required', 'file', new GuardianRule()], ]);
You can also use the facade helper:
use Guardian; $request->validate([ 'avatar' => ['required', 'file', Guardian::rule()], ]);
Manual Inspection
use Guardian; use Hassan\Guardian\Exceptions\MaliciousFileException; public function store(Request $request) { // Throws MaliciousFileException if rejected Guardian::inspect($request->file('upload')); }
If you want a result object instead of an exception:
use Guardian; public function store(Request $request) { $result = Guardian::check($request->file('upload')); if (! $result->passed) { return back()->withErrors([ 'upload' => $result->reason, ]); } }
Multiple Files
Guardian::inspectMany($request->file('documents'));
Scanner Details
1. Extension Scanner
Checks the original filename and:
- rejects files with no extension
- rejects any extension in
guardian.blocked_extensions - rejects multi-part extensions when
guardian.reject_double_extensionsistrue
Example rejected names:
shell.phpavatar.php.pngbackup.tar.php
2. MIME Scanner
Uses PHP fileinfo to detect the file's real MIME type from the file bytes, then verifies:
- the MIME exists in
guardian.allowed_mimes - the uploaded extension matches the configured extensions for that MIME
This catches renamed files whose contents do not match their extension.
3. Content Scanner
Reads up to the first 1 MB of the file and scans based on extension group:
full: runs all configured dangerous patternslight: runs a smaller high-confidence rule setskip: does not content-scan the file
Current default groups:
full:svg,txt,csv,rtf,html,htm,xmllight:jpg,jpeg,png,gif,webp,bmp,tif,tiff,ico,pdfskip: office docs, archives, audio, and video formats listed in config
Examples of patterns checked include:
- PHP opening tags
eval(),exec(),system(),shell_exec()- common obfuscation patterns
- web shell signatures
<script>tags andjavascript:payloads- null byte injection
4. SVG Scanner
Runs only for .svg files when guardian.svg_deep_scan is enabled.
It parses the SVG as XML and rejects configured dangerous elements and attributes.
Default forbidden elements include:
scriptforeignObjectfeImageiframeembedobjectanimateanimateMotionsetlink
Default forbidden attributes include a long list of event handlers such as:
onclickonloadonerroronbeginonanimationstart
It also blocks:
javascript:insidehreforxlink:href- dangerous CSS inside
style xml:base
Important: the current config intentionally allows some SVG features that older docs often block, such as legitimate <use> references and harmless visual elements, while still blocking dangerous URI and style payloads.
5. Structural Scanner
Performs format-specific validation when guardian.structural_validation is enabled.
Current behavior:
- images: validates parseability with GD, or Imagick if GD is unavailable
- SVG: validates that the file is parseable XML
- PDF: checks for the
%PDF-header - ZIP, DOCX, XLSX: checks for the
PKZIP header
If guardian.re_encode_images is true, supported images are re-encoded through GD to strip embedded payloads and metadata from the temporary uploaded file before you store it.
Structural validation currently applies to:
jpg,jpeg,png,gif,webpsvgpdfzip,docx,xlsx
Other allowed file types still pass through the earlier scanners, but do not currently receive extra structural validation.
Configuration
After publishing the config, you can tune Guardian through config/guardian.php.
Allowed MIME Map
Guardian only accepts MIME types defined in allowed_mimes.
The shipped config includes support for:
- images
- PDF and common office documents
- text files such as
txt,csv,rtf - archives such as
zip,rar,7z,gz,tar - common audio and video formats
Blocked Extensions
The default blocked list includes executable and server-side formats such as:
php,phtml,phar- shell script extensions
- Windows executable/script extensions
- Python, Ruby, Perl, CGI
- Java archive/class formats
- ASP, ASPX, JSP
.htaccess,.htpasswd
Useful Config Flags
'reject_double_extensions' => true, 'structural_validation' => true, 'scan_content' => true, 'svg_deep_scan' => true, 're_encode_images' => false, 'log_rejections' => true, 'log_channel' => env('GUARDIAN_LOG_CHANNEL', 'stack'),
Content Scan Map
'content_scan_map' => [ 'full' => ['svg', 'txt', 'csv', 'rtf', 'html', 'htm', 'xml'], 'light' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tif', 'tiff', 'ico', 'pdf'], 'skip' => ['docx', 'xlsx', 'pptx', 'doc', 'xls', 'ppt', 'zip', 'rar', '7z', 'gz', 'tar', 'mp3', 'wav', 'ogg', 'weba', 'mp4', 'mpeg', 'mpg', 'webm', 'ogv', 'avi', 'mov'], ],
Optional Size Limit
The core Guardian class also checks guardian.max_file_size if you define it in your config. The published config does not currently include this key by default, but the runtime supports it.
Example:
'max_file_size' => 5 * 1024 * 1024, // 5 MB
Rejections and Responses
Guardian::inspect() throws Hassan\Guardian\Exceptions\MaliciousFileException when a file is rejected.
The exception renders a 422 JSON response automatically:
{
"message": "File rejected by security scanner.",
"reason": "..."
}
You can also catch it manually:
use Hassan\Guardian\Exceptions\MaliciousFileException; try { Guardian::inspect($request->file('upload')); } catch (MaliciousFileException $e) { return back()->withErrors([ 'upload' => 'Your file was rejected: '.$e->getReason(), ]); }
Logging
When log_rejections is enabled, Guardian logs rejected uploads with:
- original filename
- file size
- client MIME type
- rejection reason
- scanner class
- request IP
- request URL
ClamAV Status
The repository contains a ClamAvScanner class and related config keys under guardian.clamav.
Current package state:
- the class exists
- config exists
- it is not part of the default
Guardianpipeline
That means enabling guardian.clamav.enabled in config alone does not currently add ClamAV scanning to normal Guardian::inspect() or Guardian::check() calls.
If you want to experiment with a custom pipeline, you can override the scanner list:
use Hassan\Guardian\Core\ClamAvScanner; use Hassan\Guardian\Core\ContentScanner; use Hassan\Guardian\Core\ExtensionScanner; use Hassan\Guardian\Core\MimeScanner; use Hassan\Guardian\Core\StructuralScanner; use Hassan\Guardian\Core\SvgScanner; use Hassan\Guardian\Core\Guardian as GuardianEngine; $guardian = app(GuardianEngine::class)->withScanners([ ExtensionScanner::class, MimeScanner::class, ContentScanner::class, SvgScanner::class, StructuralScanner::class, ClamAvScanner::class, ]); $guardian->inspect($request->file('upload'));
Requirements
From composer.json, the package currently requires:
- PHP
^8.1 ext-gdext-libxmlext-domext-fileinfo
Notes
- Guardian validates uploads before storage. It does not replace secure storage, safe file serving, or authorization checks.
- Browser-reported MIME types are not trusted.
- For image re-encoding to happen, GD support for the image format must be available.
- If neither GD nor Imagick is available at runtime, image structural validation is skipped with a warning log.