Laravel Unsafe Deserialization: How to Eliminate unserialize() and Prevent Object Injection
PHP unserialize() with user-controlled data enables remote code execution through object injection. Replace with json_decode() or add allowed_classes restrictions.
PHP's unserialize() function reconstructs PHP objects, arrays, and scalar values from a serialized string. Unlike json_decode(), which only produces arrays and scalar values, unserialize() can instantiate arbitrary PHP classes and call their magic methods — specifically __wakeup() and __destruct() — during the deserialization process. The vulnerability arises when attackers can control the serialized data.
PHP Object Injection attacks construct malicious serialized strings that, when deserialized, instantiate classes present in the application's autoloaded codebase and chain their magic methods to execute harmful operations. These chains — called Property Oriented Programming (POP) chains — leverage classes from frameworks, libraries, and application code. The phpggc tool maintains a library of pre-built attack chains for popular PHP frameworks including Laravel, Symfony, and Yii. An attacker with knowledge of your installed packages can select the appropriate chain and generate a working exploit payload.
The real-world severity is well documented. CVE-2021-3129 affected Laravel Ignition before version 2.5.2, exploiting a chain of calls that reached unserialize() with attacker-supplied data. The bug was classified as critical (CVSS 9.8) and resulted in remote code execution on applications with debug mode enabled. The lesson: unserialize() should be treated with the same caution as eval(). If user-controlled data can reach it through any path, the application is critically vulnerable.
The Problem
PHP's unserialize() function reconstructs objects from serialized strings. If an attacker can control the serialized data, they can instantiate arbitrary classes and trigger their __wakeup() or __destruct() methods — leading to remote code execution, file deletion, or database manipulation. This is called PHP Object Injection and is rated critical because it often leads to full server compromise. Laravel internally uses serialization for queued jobs and cache, but these are protected by encryption. The danger is when your code calls unserialize() on user-supplied input.
How to Fix
-
1
Find all unserialize() calls
Search your codebase:
grep -rn 'unserialize(' app/ routes/ --include='*.php'Also search for related functions:
grep -rn 'unserialize\|deserialize\|object_decode' app/ --include='*.php'For each occurrence, trace whether the input could originate from a user request, database field populated by users, cache, or external API.
-
2
Replace with json_decode() where possible
Most serialized data can use JSON instead:// BEFORE — unsafe $data = unserialize($request->input('data'));// AFTER — safe $data = json_decode($request->input('data'), true);JSON cannot instantiate PHP objects, eliminating the object injection vector entirely. Update both the serialization and deserialization sides:
// Saving $encoded = json_encode($data);// Loading $decoded = json_decode($encoded, true); -
3
If unserialize() is unavoidable, restrict allowed classes
PHP 7+ supports an allowed_classes option:
// Only allow specific classes $data = unserialize($serialized, [ 'allowed_classes' => [MyDTO::class, Collection::class], ]);// Or allow no classes at all (safest) $data = unserialize($serialized, [ 'allowed_classes' => false, ]);With allowed_classes set to false, any serialized objects are converted to __PHP_Incomplete_Class, preventing method execution.
How to Verify
Run the scanner:
php artisan stackshield:scan --check=SS043
Manually test: if your application accepts serialized data via any input, try sending a crafted payload and verify the application rejects it or handles it safely.
Prevention
Default to JSON for all data serialization. Never call unserialize() on user input. If you must deserialize, always use the allowed_classes option. Laravel's queue system and cache use signed/encrypted serialization — these are safe. Focus on custom code.
Frequently Asked Questions
Is Laravel's queue serialization vulnerable?
No. Laravel encrypts and signs serialized queue payloads using your APP_KEY. Attackers cannot modify the serialized data without the key. However, if your APP_KEY is compromised, queue payloads become a potential attack vector.
What about serialize() — is that dangerous too?
serialize() itself is not dangerous — it just converts data to a string. The danger is on the deserialization side with unserialize(). However, if you serialize data that will later be deserialized from an untrusted source, the full chain is vulnerable.
Are there pre-built exploit chains for Laravel applications?
Yes. The phpggc tool (available on GitHub) includes gadget chains specifically targeting Laravel, including chains through Illuminate\Broadcasting\PendingBroadcast and Illuminate\Queue\Jobs\CallQueuedHandler that enable file writes and code execution. These chains are why any unserialize() call that accepts user input is rated critical regardless of whether you think the data is safely sanitized.
If I need to store complex PHP objects, what alternatives to serialize() should I use?
Use a Data Transfer Object pattern: define a class with only scalar properties, serialize to JSON with json_encode(), and on the loading side use json_decode() into an array and map it to your DTO manually. JSON cannot instantiate PHP classes, eliminating the object injection vector. This approach is explicit about what data can be stored and retrieved.
Is base64-encoding serialized data before storing it in a cookie secure?
No. Base64 is encoding, not encryption. An attacker can base64-decode the cookie, modify the serialized payload, re-encode it, and send it back. The PHP application then unserializes attacker-controlled data. Always use Crypt::encrypt() (which uses your APP_KEY) before storing serialized data in untrusted locations like cookies or URL parameters.
Related Guides
Laravel Dangerous Function Calls: How to Eliminate eval, shell_exec, and system from Your Codebase
Functions like eval(), shell_exec(), system(), and proc_open() execute arbitrary code. If reachable from user input, they give attackers full server access.
Laravel APP_KEY Security: How to Generate, Rotate, and Protect Your Encryption Key
A missing, short, or committed APP_KEY compromises session encryption, signed URLs, and all data encrypted with Crypt. Generate a strong key and keep it out of Git.
How to Prevent SQL Injection in Laravel
SQL injection vulnerabilities in raw queries and improper Eloquent usage can expose your database. Learn how to write secure queries.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.