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.

Maintainers

Package info

github.com/hassan402-paymentrequired/guardian

pkg:composer/hassan-lateef/guardian

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.12.20 2026-03-09 10:56 UTC

This package is auto-updated.

Last update: 2026-03-09 10:59:26 UTC


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:

  1. ExtensionScanner
  2. MimeScanner
  3. ContentScanner
  4. SvgScanner
  5. StructuralScanner

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_extensions is true

Example rejected names:

  • shell.php
  • avatar.php.png
  • backup.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 patterns
  • light: runs a smaller high-confidence rule set
  • skip: does not content-scan the file

Current default groups:

  • full: svg, txt, csv, rtf, html, htm, xml
  • light: jpg, jpeg, png, gif, webp, bmp, tif, tiff, ico, pdf
  • skip: 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 and javascript: 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:

  • script
  • foreignObject
  • feImage
  • iframe
  • embed
  • object
  • animate
  • animateMotion
  • set
  • link

Default forbidden attributes include a long list of event handlers such as:

  • onclick
  • onload
  • onerror
  • onbegin
  • onanimationstart

It also blocks:

  • javascript: inside href or xlink: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 PK ZIP 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, webp
  • svg
  • pdf
  • zip, 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 Guardian pipeline

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-gd
  • ext-libxml
  • ext-dom
  • ext-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.