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()andupdated()
- 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.