PHP Serialization Vulnerabilities Explained

Khimananda Oli 8 min read Security
PHP Serialization Vulnerabilities Explained

By Khimananda Oli | Last reviewed: August 2026

PHP serialization vulnerabilities remain one of the most critical security risks in web applications because they directly enable Remote Code Execution (RCE) through object injection. When developers pass user-controlled input into unserialize(), attackers can instantiate arbitrary classes, trigger magic methods, and execute malicious payloads without needing SQL injection or XSS. This guide breaks down exactly how these attacks work, why legacy code is still vulnerable, and the concrete steps you must take to eliminate this risk in 2026.

What Are PHP Serialization Vulnerabilities and Why Do They Cause RCE?

At its core, PHP serialization vulnerabilities stem from a fundamental design flaw: unserialize() reconstructs objects by calling class constructors and magic methods automatically, before your application logic can validate them. Unlike parsing JSON or XML, which produce inert data structures, deserialization in PHP creates live objects with executable behavior attached.

User InputCrafted Payloadunserialize()Object ReconstructionMagic Methods__wakeup / __destructRCEPHP Object Injection Attack ChainAttacker controls class instantiation → triggers side effects → executes arbitrary code
PHP serialization vulnerabilities enable RCE through automatic magic method invocation during object reconstruction

The danger lies in "gadgets" — existing classes in your codebase or dependencies whose magic methods perform sensitive operations. For example, a logging class with a __destruct() method that writes files might be abused to overwrite configuration files or upload web shells. In my experience auditing Laravel and Symfony applications, I've found gadget chains in popular packages like Monolog, Guzzle, and SwiftMailer that were exploitable simply because an endpoint accepted serialized session tokens from cookies.

A common mistake is assuming that only direct $_GET or $_POST parameters are risky. In practice, vulnerabilities often hide in cookie-based sessions, cached data retrieved from Redis/Memcached, API responses stored in databases, or even HTTP headers processed by middleware. If any part of the deserialization path touches user-modifiable state, it's an attack surface. This is why understanding DevSecOps practices is essential — catching these issues requires scanning beyond obvious input points.

How Does a PHP Object Injection Attack Work Step-by-Step?

To defend against PHP serialization vulnerabilities, you need to understand the exact mechanics of exploitation. Here's the typical attack sequence:

  1. Reconnaissance: The attacker identifies an endpoint calling unserialize() on controllable data. This could be a login form storing serialized preferences in a cookie, an API accepting serialized payloads, or a cache layer reading stale data.
  2. Gadget Discovery: Using tools like phpggc or manual code review, the attacker finds classes with dangerous magic methods. A classic example is a class with __toString() that evaluates templates, or __call() that invokes dynamic functions.
  3. Payload Construction: The attacker crafts a serialized string representing the malicious object graph. For instance, O:8:"Exploit":1:{s:4:"file";s:14:"shell.php";} instantiates an Exploit class with a property pointing to a web shell path.
  4. Delivery: The payload is sent via the identified vector (cookie modification, POST body, header injection).
  5. Execution: Upon deserialization, PHP calls __wakeup() immediately, then __destruct() when the object goes out of scope. These methods execute the attacker's code before any validation occurs.
<?php
// VULNERABLE EXAMPLE - NEVER USE IN PRODUCTION
class Logger {
    public $logFile;
    
    public function __construct($file) {
        $this->logFile = $file;
    }
    
    // Magic method called automatically on destruction
    public function __destruct() {
        // Attacker controls $this->logFile via crafted payload
        file_put_contents($this->logFile, 'MALICIOUS CONTENT');
    }
}

// User input flows directly into unserialize()
$data = $_COOKIE['user_prefs']; // ATTACKER CONTROLLED
$preferences = unserialize($data); // OBJECT INJECTION OCCURS HERE
?>

In this simplified example, an attacker sets the cookie to O:6:"Logger":1:{s:7:"logFile";s:18:"/var/www/shell.php";}. When the request ends and $preferences is destroyed, __destruct() writes arbitrary content to a web-accessible path. Real-world exploits chain multiple gadgets together to bypass protections, achieve command execution, or exfiltrate database credentials. This is precisely why SAST and DAST testing should specifically flag unserialize() calls with taint analysis.

How Can You Securely Handle Unserialization in Legacy PHP Applications?

Ideally, you'd eliminate unserialize() entirely. But in legacy systems, migration takes time. PHP 7+ provides the allowed_classes parameter as a critical mitigation layer:

<?php
// SECURE PATTERN: Whitelist specific classes only
$safeData = unserialize($input, [
    'allowed_classes' => ['UserProfile', 'AppSettings']
]);

// Returns false if payload contains disallowed classes
if ($safeData === false && $input !== 'b:0;') {
    error_log('Blocked malicious serialization attempt');
    http_response_code(400);
    exit;
}
?>
Receive Serialized DataValidate Source Trust LevelUntrustedTrusted InternalREJECT or Use JSONNever unserialize()Use allowed_classesStrict whitelist onlyLog & Monitor FailuresDefense-in-depth: trust boundaries determine safe deserialization strategy
Secure unserialization decision tree based on data source trust level and allowed_classes enforcement

This approach isn't bulletproof — if a whitelisted class itself has dangerous magic methods, you're still vulnerable. Always audit every class in your allowlist for side effects in __wakeup, __destruct, __toString, __call, __get, and __set. Additionally, implement monitoring: log failed deserialization attempts as potential intrusion indicators. In SOC 2 compliance contexts, I treat repeated unserialize() failures as security incidents requiring investigation, not just application errors.

For session handling specifically, switch to native PHP sessions with database or Redis handlers that don't rely on serialization, or use JWT tokens signed with HMAC-SHA256. Never store serialized objects in cookies unless absolutely necessary, and always encrypt + authenticate cookie contents with libsodium if you must.

JSON vs PHP Serialize: Which Format Should You Use in 2026?

The definitive answer is JSON for all new development and migrations. Here's a practical comparison based on real production trade-offs:

CriteriaPHP serialize()JSON (json_encode/decode)
SecurityInherently unsafe with untrusted input; enables RCESafe by default; produces inert arrays/objects
Type PreservationPreserves PHP types, private properties, class namesLoses type info; no private/protected access
InteroperabilityPHP-only format; unreadable elsewhereUniversal standard; works across all languages
PerformanceFaster for complex nested PHP objectsSlightly slower but negligible for most use cases
Human ReadabilityBinary-like opaque stringsClean, debuggable text format
Magic Method RiskTriggers __wakeup/__destruct automaticallyNo automatic method invocation

JSON sacrifices some fidelity (no private properties, no circular references) but eliminates the entire category of PHP serialization vulnerabilities. When you need type safety, use explicit DTOs with validation libraries like spatie/data-transfer-object or Symfony Serializer with strict denormalization. For caching where performance matters, consider MessagePack or Protocol Buffers as safer binary alternatives that don't execute code during decoding.

PHP serialize()!RCE via Object Injection!Gadget Chain Exploits!Magic Method Side EffectsHIGH RISK — Avoid with untrusted dataJSONNo Code ExecutionLanguage AgnosticInert Data StructuresSAFE BY DEFAULT — Recommended for 2026MIGRATE
Security comparison: JSON eliminates PHP serialization vulnerabilities while serialize() retains inherent RCE risks

How Do You Detect and Audit Existing Serialization Risks in Your Codebase?

Finding hidden unserialize() calls requires systematic searching. Start with static analysis:

# Find all unserialize calls recursively
grep -rn "unserialize(" --include="*.php" ./src ./vendor

# Check for dangerous magic methods in your codebase
grep -rn "__wakeup\|__destruct\|__toString\|__call" --include="*.php" ./src

# Scan composer dependencies for known gadget chains
composer audit
./vendor/bin/phpggc --list-gadgets

Integrate SAST tools like Rector, Psalm, or SonarQube with custom rules flagging unserialize() without allowed_classes. In CI pipelines, fail builds that introduce new unprotected deserialization. For runtime detection, wrap unserialize() in a custom function that logs stack traces and validates inputs before processing. This visibility is crucial for vulnerability management automation and maintaining audit trails for compliance frameworks.

Pay special attention to third-party libraries. Many popular packages historically used serialization internally (e.g., caching adapters, queue drivers). Update aggressively — maintainers have largely migrated away from unsafe patterns since 2020, but outdated vendor directories remain a top infection vector. Run dependency scans weekly and prioritize updates for packages with known CVEs related to deserialization.

Practical Remediation Steps for Production Systems

If you've identified vulnerable code, follow this prioritized remediation plan:

  • Immediate: Add allowed_classes => false to all unserialize() calls handling external data. This blocks object instantiation entirely while preserving array/string deserialization.
  • Short-term: Replace cookie/session serialization with JSON or encrypted JWTs. Migrate cache layers to MessagePack or igbinary (which doesn't invoke magic methods).
  • Medium-term: Refactor data transfer to explicit DTOs with validation. Remove unnecessary magic methods from domain models.
  • Long-term: Adopt schema-driven APIs (OpenAPI, Protobuf) that enforce structure at the boundary. Implement WAF rules blocking known serialized payload signatures as defense-in-depth.

Document every change and verify with regression tests. Serialization bugs often manifest subtly — missing properties, broken inheritance, lost references. Test thoroughly in staging before deploying fixes to production environments serving Nepali or global users who expect reliability alongside security.

Eliminate PHP Serialization Vulnerabilities Before They Exploit You

PHP serialization vulnerabilities aren't theoretical — they're actively exploited in the wild against WordPress plugins, Laravel apps, and custom PHP backends daily. The fix is straightforward: stop using unserialize() on untrusted data, migrate to JSON or safer binary formats, and enforce strict class allowlists where legacy constraints demand it. Treat every unserialize() call as a potential RCE until proven otherwise through code review and automated scanning. If your team needs help auditing legacy PHP systems, implementing secure serialization patterns, or preparing for SOC 2 compliance with proper evidence collection, reach out for a security assessment. Don't wait for an incident to validate what static analysis already tells you.

Frequently Asked Questions

It occurs when untrusted data is passed to unserialize(), allowing attackers to instantiate arbitrary objects and execute malicious code through magic methods like wakeup or destruct during the deserialization process.

Object injection exploits application logic by manipulating class state via serialized strings, whereas SQL injection targets database queries. Serialization attacks bypass input validation by abusing trusted internal classes rather than corrupting query syntax directly.

The wakeup, destruct, and toString methods are primary vectors because they execute automatically upon object creation or string conversion, enabling attackers to trigger file operations, command execution, or database writes without explicit calls.

No, json_decode returns arrays or stdClass objects without invoking magic methods. Only native unserialize() triggers automatic method execution, making JSON parsing inherently safer for handling external data structures in modern PHP applications.

Use JSON encoding for public data or authenticated encryption with libsodium for sensitive state. Avoid storing serialized PHP objects in cookies, caches, or databases where attackers could modify the payload before deserialization occurs.

Run Rector with the UnsafeUnserializeRule or use PHPStan level 9 to flag dynamic unserialize usage. Grep codebases for unserialize($variable) patterns where the variable originates from user input, session data, or external APIs.

It restricts instantiation to specified classes but does not eliminate risk if whitelisted classes contain dangerous magic methods. Audit every allowed class for side effects in constructors, destructors, and wakeup handlers before permitting deserialization.

Property-Oriented Programming chains link multiple benign classes together so that one magic method triggers another, eventually reaching a sink like system() or file_put_contents(). Attackers reuse existing code rather than injecting new payloads.

WAFs struggle because serialized payloads often appear as valid base64 or hex strings without obvious signatures. Application-level validation and avoiding unserialize on untrusted data remain the only reliable defenses against these attacks.

Eloquent models implement Serializable and define wakeup for lazy loading. If model instances are deserialized from user-controlled sources, attackers can manipulate attributes to trigger database queries or access unauthorized resources during rehydration.

PHP 8.4 enforces stricter type checking during unserialization and deprecates implicit magic method invocation in certain contexts. These changes reduce gadget surface area but do not replace the need for input validation and safe alternatives.

Yes, if session.save_handler uses php_serialize and session IDs are predictable or exposed. Attackers inject crafted serialized data into session files, which the engine automatically unserializes on subsequent requests, triggering object injection.

PHPGGC generates POP chains for popular frameworks like Symfony, Laravel, and WordPress. Security testers use it to validate whether specific library versions contain exploitable gadget chains that lead to remote code execution.

Strict typing helps at function boundaries but cannot stop unserialize() from creating objects before type checks occur. Validation must happen before deserialization, not after, since the dangerous magic methods execute during object construction.

Encoding provides zero security benefit since attackers easily decode payloads before submission. Base64 merely obscures content from casual inspection while remaining trivially reversible, offering no protection against intentional deserialization exploitation attempts.