Security guides often stay theoretical — until your app is actually compromised.

This post documents real security failures, how the attack worked, and how to harden a Laravel application properly, based on a real incident involving:

  • Malicious file uploads
  • Queue poisoning
  • Livewire object hydration abuse
  • Serialized job injection
  • RCE payloads hidden in unexpected places

If you run a Laravel app that accepts uploads, uses queues, Livewire, or notifications — this applies to you.

1. The Root Cause: Trusting User Input Too Much

The attack did not start with a visible exploit.

Instead, it abused multiple small assumptions:

  • “Laravel validation is enough”
  • “Storage files can’t execute”
  • “Queue jobs are internal”
  • “Livewire only handles frontend data”
  • “No POST request = no upload”

Each assumption was technically true — but dangerously incomplete.

Security must be layered, not singular.

2. Locking Down File Uploads (Correctly)

❌ Common mistakes

  • Validating by extension only
  • Trusting client-provided MIME types
  • Storing uploads in executable paths
  • Iterating over $request->files->all() blindly

✅ Correct approach

A. Validate strictly

$request->validate([
'video' => [
'required',
'file',
'max:102400', // 100MB
'mimetypes:video/mp4,video/quicktime,video/x-msvideo,video/x-matroska',
],
]);

⚠️ Important:
Laravel’s mimetypes uses PHP’s finfo internally — but only if the file is accessed correctly.

Always retrieve files explicitly:

$file = $request->file('video');

Never loop over:

$request->files->all();

That bypasses validation expectations.

B. Enforce real MIME validation (custom rule)

Create a Real MIME rule using finfo:

                    use Illuminate\Contracts\Validation\ValidationRule;

class RealMimeType implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! $value instanceof \Illuminate\Http\UploadedFile) {
            $fail('Invalid file.');
            return;
        }

        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $mime = $finfo->file($value->getRealPath());

        $allowed = [
            'video/mp4',
            'video/quicktime',
            'video/x-msvideo',
            'video/x-matroska',
        ];

        if (! in_array($mime, $allowed, true)) {
            $fail('Invalid real MIME type.');
        }
    }
}

                  

How to use it:

                    'video' => ['required', 'file', new RealMimeType],
                  

3. Storage Hardening (This Is Critical)

❌ The dangerous default

Saving user files in:

storage/app/public

…while exposing them via:

public/storage

Even with .htaccess, misconfiguration or symlink abuse can lead to execution.

✅ Best practice

A. Store uploads outside web root

                    'local' => [
    'driver' => 'local',
    'root' => storage_path('app'),
    'throw' => true,
],

                  

Then expose files only via controllers:

                    return response()->file(storage_path('app/videos/'.$filename));

                  

B. Enforce non-executable permissions

                    chmod -R 0640 storage/
find storage -type d -exec chmod 0750 {} \
                  

C. Defense-in-depth .htaccess (still useful)

                    # ==================================================
# ABSOLUTE: NO SCRIPT EXECUTION — EVER
# ==================================================

# Disable PHP engine (mod_php safety)
<IfModule mod_php.c>
    php_flag engine off
</IfModule>

# Block execution of scripts (Apache + PHP-FPM safe)
<FilesMatch "\.(php|phtml|phar|php[0-9]*|pl|py|jsp|asp|sh|cgi|exe)$">
    Require all denied
</FilesMatch>

# Remove all PHP handlers (prevents handler abuse)
RemoveHandler .php .phtml .phar .php8 .php7 .php5
RemoveType .php .phtml .phar .php8 .php7 .php5

# Disable directory listing
Options -Indexes

# Force safe MIME types (prevents PHP parsing)
AddType text/plain .php .phtml .phar .php8 .php7 .php5

# Prevent .htaccess override chaining
AllowOverride None

                  

this .htaccess file you should store in storage/app/public and it will prevent execution of any executable files

4. Global RCE Payload Detection (Before Laravel Touches It)

The most important lesson:

Detection must happen before Livewire, validation, jobs, or queues.

✅ Create a global middleware

php artisan make:middleware DetectRcePayload

                    class DetectRcePayload
{
    public function handle(Request $request, Closure $next)
    {
        if (! $request->isMethod('get')) {
            $payload = json_encode($request->all(), JSON_UNESCAPED_SLASHES);

            if ($payload && preg_match('/\b(wget|curl|perl|bash|sh|base64)\b/i', $payload)) {

                Log::alert('Possible RCE payload detected', [
                    'ip' => $request->ip(),
                    'url' => $request->fullUrl(),
                    'user_agent' => $request->userAgent(),
                ]);

                abort(403, 'Forbidden');
            }
        }

        return $next($request);
    }
}

                  

Register it first in Kernel.php:

                    protected $middleware = [
    \App\Http\Middleware\DetectRcePayload::class,
    // other middleware
];

                  

This alone blocks queue poisoning, Livewire injection, and serialized payloads.

5. Queue Hardening: Prevent Job Injection

The attack inserted a serialized BroadcastEvent job with shell commands inside.

Laravel will execute queued jobs blindly unless you stop it.

✅ Block dangerous jobs at the worker level

In AppServiceProvider:

                    use Illuminate\Queue\Events\JobProcessing;

public function boot()
{
    Queue::before(function (JobProcessing $event) {
        $payload = $event->job->payload();

        $blocked = [
            'Illuminate\\Broadcasting\\BroadcastEvent',
            'Illuminate\\Queue\\CallQueuedHandler',
        ];

        if (in_array($payload['displayName'] ?? '', $blocked, true)) {
            Log::critical('Blocked dangerous job', $payload);
            $event->job->delete();
        }
    });
}

                  

🔒 Bonus: Use Redis instead of database queues

Database queues make inspection easier for attackers.

6. Livewire: The Silent Attack Vector

Livewire hydrates public properties from user input.

If unchecked, it can:

  • Deserialize objects
  • Trigger broadcasts
  • Queue jobs
  • Execute destructors

✅ Livewire hardening checklist

  • Avoid storing arrays from user input in public properties
  • Never store objects in public properties
  • Validate inside mount() and updated()
  • Treat $this->dispatch() as untrusted
  • Upgrade Livewire to the latest version
                    protected function rules()
{
    return [
        'notificationMessage' => 'string|max:255',
    ];
}

                  

7. Disable Exposed Server Endpoints

❌ This should never be public:

/whm-server-status
/server-status

Disable immediately.

8. Rotate & Revoke ALL Compromised Keys

If .env was ever readable or uploaded:

  • ❌ PayPal API key
  • ❌ AWS access key
  • ❌ Google API key
  • ❌ TikTok API key

✅ Required steps

  • Rotate keys
  • Revoke old ones
  • Audit usage logs
  • Move secrets to a vault or env-only config

9. Logging & Forensics

Logs to inspect:

  • Apache access/error logs
  • Laravel logs
  • Queue tables
  • Failed jobs
  • Uploaded file timestamps (stat)

Look for:

  • Non-POST uploads
  • Serialized payloads
  • Unexpected queue jobs
  • Shell commands in logs

10. Final Security Checklist

✅ Strict file validation (real MIME)

✅ Store uploads outside web root

✅ Global RCE middleware

✅ Queue job denylist

✅ Livewire property validation

✅ Disable server-status
 ✅ Rotate secrets

✅ Harden permissions

✅ Monitor logs

Cleanup

Check do we have any .php file stored anywhere in storage folders:

                    find storage/ -type f -name "*.php"
                  

you can manually delete those files, or use command:

                    find storage/ -type f -name "*.php" -delete

                  

Check for running processes, run this in terminal:
ps auxf | grep php sicter

the output will show you running processes, and look for suspicious ones - for example php is executed from strange location, in this case from /tmp/httpd.conf

                    2752186 0.2 0.0 442888 42332 ? S 2025 17:28 /opt/cpanel/ea-php82/root/usr/bin/php -f /tmp/httpd.conf sicter ==> these are maliciois, php is executed from tmp/httpd.conf file
2752188 0.2 0.0 442888 42328 ? S 2025 17:38 /opt/cpanel/ea-php82/root/usr/bin/php -f /tmp/httpd.conf sicter ==> these are maliciois, php is executed from tmp/httpd.conf file
2752195 0.2 0.0 442888 42240 ? S 2025 17:34 /opt/cpanel/ea-php82/root/usr/bin/php -f /tmp/httpd.conf sicter ==> these are maliciois, php is executed from tmp/httpd.conf file
2752200 0.2 0.0 442888 42284 ? S 2025 17:32 /opt/cpanel/ea-php82/root/usr/bin/php -f /tmp/httpd.conf sicter ==> these are maliciois, php is executed from tmp/httpd.conf file
                  

Next, you should stop these processes:

                    kill -9 2752186 2752188 2752195 2752200
                  

Also, check your index.php file, it could happen that some of these processes did include malicious php code in there.

Final Thoughts

Laravel is not insecure.

But modern attacks no longer target “Laravel vulnerabilities” —
they target assumptions between features.

Security today means:

Assume every boundary will be crossed.