PHP unserialize() Deserialization Vulnerability

Critical Risk Deserialization
phpunserializedeserializationobject-injectionremote-code-execution

What it is

The PHP application uses the unserialize() function to process untrusted data, which can lead to object injection attacks and remote code execution. When unserialize() processes malicious serialized data, it can instantiate arbitrary classes and trigger dangerous magic methods like __wakeup() or __destruct().

// Vulnerable: Unsafe unserialize() usage // Example 1: Direct unserialize of user input if (isset($_POST['data'])) { // Extremely dangerous: User can inject arbitrary objects $userData = unserialize($_POST['data']); processUserData($userData); } // Example 2: Cookie-based serialized data if (isset($_COOKIE['user_prefs'])) { // Dangerous: Cookies can be modified by users $preferences = unserialize(base64_decode($_COOKIE['user_prefs'])); applyUserPreferences($preferences); } // Example 3: Database-stored serialized data (still risky) function loadUserSession($sessionId) { $sql = "SELECT session_data FROM sessions WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$sessionId]); $row = $stmt->fetch(); if ($row) { // Dangerous if session_data was tampered with return unserialize($row['session_data']); } return null; } // Example of dangerous class class FileManager { private $filename; public function __construct($filename) { $this->filename = $filename; } // Dangerous magic method public function __destruct() { if ($this->filename) { unlink($this->filename); // Could delete arbitrary files } } }
// Secure: Safe alternatives to unserialize() // Alternative 1: Use JSON instead of serialize function storeUserPreferences($preferences) { // Safe: JSON encoding $jsonData = json_encode($preferences, JSON_THROW_ON_ERROR); $encoded = base64_encode($jsonData); // Set secure cookie setcookie('user_prefs', $encoded, [ 'expires' => time() + 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict' ]); } function loadUserPreferences() { if (!isset($_COOKIE['user_prefs'])) { return []; } try { $jsonData = base64_decode($_COOKIE['user_prefs']); // Safe: JSON decoding $preferences = json_decode($jsonData, true, 10, JSON_THROW_ON_ERROR); // Validate structure if (!is_array($preferences)) { throw new InvalidArgumentException('Invalid preferences format'); } return $preferences; } catch (Exception $e) { error_log('Preferences loading error: ' . $e->getMessage()); return []; } } // Alternative 2: Restricted unserialize with allowed classes class SafeSessionManager { private $allowedClasses = [ 'UserData', 'UserPreferences' ]; public function deserializeSessionData($serializedData, $signature) { // Verify signature first if (!$this->verifySignature($serializedData, $signature)) { throw new SecurityException('Invalid session signature'); } try { // Restrict to allowed classes only $data = unserialize($serializedData, [ 'allowed_classes' => $this->allowedClasses ]); return $data; } catch (Exception $e) { throw new SecurityException('Session deserialization failed'); } } public function serializeSessionData($data) { $serialized = serialize($data); $signature = $this->createSignature($serialized); return ['data' => $serialized, 'signature' => $signature]; } private function createSignature($data) { $key = getenv('SESSION_SIGNING_KEY'); return hash_hmac('sha256', $data, $key); } private function verifySignature($data, $signature) { $expectedSignature = $this->createSignature($data); return hash_equals($expectedSignature, $signature); } } // Alternative 3: Safe data transfer objects class UserData { private $id; private $username; private $email; public function __construct($id, $username, $email) { $this->id = (int) $id; $this->username = filter_var($username, FILTER_SANITIZE_STRING); $this->email = filter_var($email, FILTER_VALIDATE_EMAIL); if (!$this->email) { throw new InvalidArgumentException('Invalid email address'); } } // Safe: No dangerous magic methods public function toArray() { return [ 'id' => $this->id, 'username' => $this->username, 'email' => $this->email ]; } public static function fromArray($data) { if (!isset($data['id'], $data['username'], $data['email'])) { throw new InvalidArgumentException('Missing required fields'); } return new self($data['id'], $data['username'], $data['email']); } } // Secure usage try { $sessionManager = new SafeSessionManager(); // When storing session $userData = new UserData(1, 'john_doe', 'john@example.com'); $sessionData = $sessionManager->serializeSessionData($userData); // When loading session $loadedData = $sessionManager->deserializeSessionData( $sessionData['data'], $sessionData['signature'] ); } catch (Exception $e) { error_log('Session management error: ' . $e->getMessage()); // Handle error securely }

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

PHP applications call unserialize() directly on user-supplied data from cookies, POST/GET parameters, HTTP headers, or uploaded files without understanding that unserialize() instantiates arbitrary objects enabling object injection attacks. Common vulnerable patterns include session management deserializing cookie data: $session = unserialize(base64_decode($_COOKIE['session'])) allowing attackers to craft malicious serialized objects in cookies, form processing accepting serialized input: $formData = unserialize($_POST['data']) intended for complex data structures but exploitable for arbitrary object instantiation, and API endpoints accepting serialized payloads: $request = unserialize(file_get_contents('php://input')) in REST APIs or SOAP services. Attackers craft malicious serialized strings exploiting application classes with dangerous magic methods (__destruct(), __wakeup(), __toString(), __call()) that execute during deserialization or object destruction. Example attack: O:11:"FileManager":1:{s:17:"\0FileManager\0file";s:16:"/etc/passwd.bak";} creates FileManager instance with arbitrary file path property, triggering __destruct() that deletes specified file. More sophisticated attacks chain multiple objects creating POP (Property-Oriented Programming) chains similar to ROP in binary exploitation: attacker constructs object graph where properties point to other objects, calling magic methods in sequence executing complex attack logic culminating in code execution, file operations, or SQL injection. The vulnerability is particularly severe because: PHP unserialize() supports all classes loaded in application including framework classes, vendor libraries, and application code providing vast attack surface, magic methods execute automatically without explicit code calling them, serialized data format allows precise control over object state including private properties, and deserialization happens before any application-level validation enabling exploitation regardless of post-deserialization checks. Real-world exploitation examples include WordPress plugin vulnerabilities where unserialize() on user input enabled remote code execution, Magento eCommerce platform object injection vulnerabilities allowing admin account takeover, and numerous PHP framework deserialization chains discovered in Laravel, Symfony, Zend Framework, and Yii.

Root causes

Using unserialize() on User-Controlled Input like Cookies or POST Data

PHP applications call unserialize() directly on user-supplied data from cookies, POST/GET parameters, HTTP headers, or uploaded files without understanding that unserialize() instantiates arbitrary objects enabling object injection attacks. Common vulnerable patterns include session management deserializing cookie data: $session = unserialize(base64_decode($_COOKIE['session'])) allowing attackers to craft malicious serialized objects in cookies, form processing accepting serialized input: $formData = unserialize($_POST['data']) intended for complex data structures but exploitable for arbitrary object instantiation, and API endpoints accepting serialized payloads: $request = unserialize(file_get_contents('php://input')) in REST APIs or SOAP services. Attackers craft malicious serialized strings exploiting application classes with dangerous magic methods (__destruct(), __wakeup(), __toString(), __call()) that execute during deserialization or object destruction. Example attack: O:11:"FileManager":1:{s:17:"\0FileManager\0file";s:16:"/etc/passwd.bak";} creates FileManager instance with arbitrary file path property, triggering __destruct() that deletes specified file. More sophisticated attacks chain multiple objects creating POP (Property-Oriented Programming) chains similar to ROP in binary exploitation: attacker constructs object graph where properties point to other objects, calling magic methods in sequence executing complex attack logic culminating in code execution, file operations, or SQL injection. The vulnerability is particularly severe because: PHP unserialize() supports all classes loaded in application including framework classes, vendor libraries, and application code providing vast attack surface, magic methods execute automatically without explicit code calling them, serialized data format allows precise control over object state including private properties, and deserialization happens before any application-level validation enabling exploitation regardless of post-deserialization checks. Real-world exploitation examples include WordPress plugin vulnerabilities where unserialize() on user input enabled remote code execution, Magento eCommerce platform object injection vulnerabilities allowing admin account takeover, and numerous PHP framework deserialization chains discovered in Laravel, Symfony, Zend Framework, and Yii.

Processing Serialized Data from Untrusted Sources

Applications treat serialized data from databases, file systems, message queues, caching systems, or external APIs as trusted despite potential tampering or compromise, then unserialize without validation. Database-stored sessions: session.save_handler = user with custom session handlers storing serialized data in MySQL/PostgreSQL/Redis, then unserialize($_SESSION['data']) trusting database contents, but vulnerable when SQL injection allows modifying session data or when database backups restore attacker-modified sessions. Cache poisoning: applications cache serialized objects in Memcached or Redis: $cached = unserialize($redis->get($key)), exploitable when attackers gain cache access through misconfiguration (publicly accessible Memcached/Redis), cache injection vulnerabilities (cache key manipulation allowing attackers to set arbitrary keys), or compromised cache infrastructure. Message queue deserialization: worker processes consuming jobs from RabbitMQ, Beanstalkd, or AWS SQS unserialize job payloads: $job = unserialize($message->body), vulnerable when message queues lack authentication allowing unauthorized job submission, when one compromised service submits malicious jobs affecting others, or during queue replay attacks. File-based serialization: applications storing serialized data in files then reading: unserialize(file_get_contents($filepath)), exploitable through path traversal allowing attackers to control deserialized content, file upload vulnerabilities letting attackers upload malicious serialized files, or backup restoration using attacker-modified backups. API integration deserializing responses: consuming SOAP services, legacy APIs, or inter-service communication using serialize/unserialize: $response = unserialize($apiResponse), trusting external services without validating serialized structure despite man-in-the-middle attacks, compromised upstream services, or malicious service providers. The fundamental security error is treating serialized format as data serialization format rather than code serialization format—serialized PHP objects contain executable code paths through magic methods, making deserialization from any untrusted source equivalent to eval() on untrusted input. Defense-in-depth failures compound the issue: applications lacking principle of least privilege where database or cache access enables serialized data modification, missing network segmentation allowing attackers to reach internal data stores, inadequate monitoring failing to detect unauthorized access to session stores or caches, and insufficient encryption/authentication protecting serialized data in transit and at rest.

Missing Validation of Serialized Data Before Deserialization

Developers attempt to make unserialize() safe through validation but implement insufficient checks allowing bypass, or validate after deserialization when exploitation already occurred. Inadequate validation patterns include checking only data format: if (preg_match('/^[a-zA-Z0-9\+\/=]+$/', $input)) { $data = unserialize(base64_decode($input)); } verifying base64 encoding but not serialized content structure, validating data length without content inspection: if (strlen($serialized) < 10000) { $data = unserialize($serialized); } preventing only large payloads, and blacklist approaches attempting to block dangerous strings: if (!preg_match('/eval|system|exec/', $serialized)) { unserialize($serialized); } easily bypassed as serialized format uses cryptic syntax and class names rather than function calls. Post-deserialization validation provides no protection: $obj = unserialize($input); if ($obj instanceof SafeClass) { /* use $obj */ } because dangerous magic methods (__wakeup(), __destruct()) execute during unserialize() before instanceof check, object destruction at script end triggers __destruct() regardless of validation, and POP chain exploitation happens through property access and method calls during deserialization process not during object usage. Type checking fails: validating that deserialized result is certain type doesn't prevent exploitation as attackers instantiate dangerous classes that later get destroyed triggering exploits, or use POP chains where safe outer object contains dangerous inner objects exploiting composition. String inspection of serialized data misses sophisticated attacks: searching for dangerous class names in serialized string: if (strpos($serialized, 'FileManager') === false) { unserialize($serialized); } fails because class names can be partial matches, inheritance allows exploiting parent classes, and nested objects hide dangerous classes inside safe outer objects. Attempting to validate by deserializing safely then re-serializing: unserialize($input, ['allowed_classes' => []]) to parse without instantiating, then comparing structure, fails because attackers exploit the very fact that even restricted deserialization parses object graph enabling attacks, and re-serialization may not match original if attacker used crafted serialization. The only effective validation is cryptographic: HMAC signature verification using secret key unknown to attackers ensuring serialized data originated from trusted source and wasn't modified. Without cryptographic verification, no amount of pattern matching, type checking, or structural validation prevents unserialize() exploitation.

Presence of Dangerous Classes with Magic Methods in the Application

Application codebases, frameworks, and dependencies contain classes implementing magic methods (__destruct(), __wakeup(), __toString(), __call(), __get(), __set()) whose execution during deserialization enables exploitation even without direct vulnerable code paths. Common dangerous patterns include classes with __destruct() performing file operations: class Logger { public function __destruct() { file_put_contents($this->logfile, $this->logdata); } } allowing attackers to write arbitrary content to arbitrary files by instantiating Logger with controlled properties during unserialize(). Classes with __wakeup() or __destruct() executing database queries: class Cache { public function __destruct() { $this->db->query("DELETE FROM cache WHERE key = '" . $this->key . "'"); } } enabling SQL injection when $this->key contains attacker-controlled values. Classes calling other methods within magic methods: class Request { public function __wakeup() { $this->dispatch(); } public function dispatch() { $this->execute($this->handler); } } creating method chaining where controlled properties determine execution flow. Template engines with __toString() rendering templates: class Template { public function __toString() { return $this->render($this->template, $this->data); } } exploitable when object converted to string during error handling, logging, or concatenation. Framework classes with complex dependencies creating POP chains: Symfony's PropertyAccess component, Laravel's PendingBroadcast, Doctrine's AbstractQuery, and similar classes combine to form multi-stage exploitation where property access triggers getter methods calling other objects' methods ultimately executing arbitrary code. Third-party library classes increase attack surface: applications depending on dozens of Composer packages inherit all classes with exploitable magic methods from dependencies, security updates adding new methods create new gadgets, and developers unaware of dependency internals cannot assess deserialization risk. The problem compounds through PHP's dynamic features: __call() magic method catching all undefined method calls allowing method chaining through non-existent methods, __get() enabling property access gadgets, __toString() triggered implicitly when objects used in string contexts during logging or error handling, and __invoke() executing objects as functions. Secure coding practices paradoxically increase risk: extensive use of magic methods for API elegance provides more exploitation vectors, sophisticated framework architectures with dependency injection and event systems create complex object graphs exploitable as POP chains, and rich ORM libraries with lazy loading and proxy objects generate serialization gadgets. Automated POP chain discovery tools (PHPGGC) catalog known gadget chains for popular frameworks enabling attackers to weaponize deserialization vulnerabilities efficiently.

Storing Serialized Objects in Databases Without Proper Validation

Applications persist serialized PHP objects in database columns (often TEXT or BLOB fields) for storing complex data structures, then unserialize without validating data integrity or considering SQL injection risks enabling deserialization exploitation. Common patterns include storing user preferences as serialized arrays: UPDATE users SET preferences = ? WHERE id = ? with serialize($prefs) allowing SQL injection or direct database manipulation to inject malicious serialized objects, session storage in databases using session.save_handler=user: custom session handlers storing serialized $_SESSION data that applications trust implicitly on retrieval, and ORM attribute serialization: Doctrine's object type or Laravel's casts arrays automatically serializing/unserializing object properties. Exploitation vectors include SQL injection enabling serialized data modification: attackers exploit SQL injection to UPDATE session_data column with malicious serialized payload affecting other users' sessions, providing instant remote code execution when victim deserializes compromised session. Database backups restore malicious data: attackers compromise database or backup systems, modify serialized columns, then wait for legitimate backup restoration introducing malicious objects. Privilege escalation through database access: attackers gaining read/write database access through misconfiguration, compromised application logic, or hosting control panel exploit privileges to modify any serialized column. Stored XSS or SQLi as stepping stone: attackers initially exploit unrelated vulnerabilities gaining capability to modify database contents then pivot to deserialization attacks. Time-delayed exploitation: attackers inject malicious serialized data through any database modification vector knowing future deserialization will trigger exploitation—vulnerability lies dormant until deserialization occurs. Multi-tenant database exploitation: in SaaS platforms sharing databases, one tenant exploiting SQL injection modifies serialized data in another tenant's records enabling cross-tenant attacks. Legacy migration creating exposure: applications migrating from file-based sessions or old data formats to database storage bringing serialized data from insecure sources without validation. Disaster recovery scenarios: restoring databases from compromised backups, importing data from untrusted sources during migrations, or database replication from compromised secondary servers introducing malicious serialized content. The core issue is treating databases as trusted storage for executable code (serialized objects) without defense-in-depth: missing encryption preventing offline database modification, no integrity verification (HMAC) detecting tampering, over-privileged database users allowing unauthorized modification, and insufficient monitoring detecting anomalous serialized data changes. Even read-only access to database enables offline POP chain crafting: attackers analyze database-stored serialized objects learning application class structure, examine available classes identifying exploitable magic methods, develop POP chains targeting specific application classes, then inject crafted serialized payloads through any available database modification vector.

Fixes

1

Use JSON Encoding/Decoding Instead of serialize/unserialize

Migrate from PHP's serialize/unserialize to JSON encoding for data persistence and transmission, eliminating object instantiation risks inherent to PHP serialization. Replace serialize() with json_encode(): $jsonData = json_encode($data, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); ensuring JSON_THROW_ON_ERROR flag throws exceptions on encoding failures rather than returning false. Replace unserialize() with json_decode(): $data = json_decode($jsonData, true, 512, JSON_THROW_ON_ERROR); using associative array mode (second parameter true) preventing object instantiation, limiting recursion depth (512 default reasonable), and throwing exceptions on malformed JSON. JSON advantages include: only serializes scalar values and arrays without objects/resources eliminating object injection, language-agnostic format enabling interoperability with non-PHP systems, simpler syntax easier to validate and inspect, no magic method execution during decoding, and JSON_THROW_ON_ERROR mode providing clear error handling. Implement proper validation post-JSON-decode: after $data = json_decode($json, true), validate structure using: is_array($data) checking expected type, array_key_exists() verifying required keys, is_int(), is_string(), is_bool() type-checking individual values, and custom validation ensuring business logic constraints. Use JSON Schema validation for complex structures: employ libraries like justinrainbow/json-schema: $validator->validate($data, $schema); defining expected JSON structure in schema file. For nested data structures requiring objects, reconstruct from validated arrays: class UserDTO { public static function fromArray(array $data): self { /* validate and construct */ } public function toArray(): array { /* convert to array */ } }, ensuring controlled object construction from trusted validated data rather than automatic unserialize. Handle JSON encoding/decoding errors: wrap in try-catch handling JsonException (PHP 7.3+) or check json_last_error(): if (json_last_error() !== JSON_ERROR_NONE) { throw new RuntimeException(json_last_error_msg()); }. Database schema changes: modify BLOB/TEXT columns storing serialized data to JSON columns (MySQL 5.7+, PostgreSQL 9.2+ JSONB) enabling database-level JSON querying and validation. Migration strategy: implement gradual migration using feature flags: if (USE_JSON_SERIALIZATION) { /* json */ } else { /* serialize */ }, maintain backward compatibility during transition reading both formats: try json_decode first, fallback to unserialize for legacy data, then re-save as JSON, and schedule complete migration removing serialize/unserialize within defined timeframe. Test compatibility: ensure JSON serialization preserves all necessary data (JSON cannot serialize PHP resources, closures, or custom objects), test round-trip encoding/decoding maintaining data integrity, and benchmark performance comparing JSON vs serialize (JSON generally faster). Document exceptions: rare cases legitimately requiring object serialization (caching ORM entities, complex dependency-injected objects) must use alternative solutions like MessagePack with object mapping or Protocol Buffers with strict schemas rather than unserialize.

2

Implement allowed_classes Option to Restrict Deserialization

When eliminating unserialize() entirely is infeasible, use PHP 7.0+ allowed_classes option restricting which classes can be instantiated during deserialization, significantly reducing attack surface. Configure allowed_classes explicitly: unserialize($data, ['allowed_classes' => ['UserPreferences', 'SessionData']]) permitting only specified safe classes, any other class in serialized data becomes __PHP_Incomplete_Class preventing exploitation. Use empty array for complete restriction: unserialize($data, ['allowed_classes' => []]) deserializes only standard PHP types (stdClass, arrays, scalars) converting any class instances to __PHP_Incomplete_Class. Define safe classes carefully: only allow classes verified to lack dangerous magic methods (__destruct, __wakeup, __toString, etc.), ensure allowed classes don't have properties that are themselves exploitable objects creating nested exploitation, and audit class implementations ensuring no file operations, code execution, or sensitive operations in constructors or magic methods. Create allowlist management: maintain centralized allowlist configuration: class SerializationConfig { const ALLOWED_CLASSES = ['DTO\UserData', 'DTO\ProductData']; } function safeDe serialize($data) { return unserialize($data, ['allowed_classes' => SerializationConfig::ALLOWED_CLASSES]); }, document why each class is allowlisted with security justification, and review allowlist during security audits removing classes no longer needed or adding new classes with proper review. Implement validation after restricted deserialization: even with allowed_classes, validate deserialized data: $obj = unserialize($data, ['allowed_classes' => $allowedClasses]); if (!$obj instanceof ExpectedClass) { throw new SecurityException('Unexpected type'); }, check object state: validate properties contain expected values/types, and sanitize any user-controlled data within object properties. Use Data Transfer Objects (DTOs): design classes specifically for serialization with minimal logic: class UserDTO { public $id; public $name; /* no magic methods */ }, DTOs should have no magic methods, no business logic in constructors, only public properties with simple types, and explicit validation methods called post-deserialization. Combine with HMAC signature verification: allowed_classes prevents instantiating dangerous classes but not property injection in allowed classes, so verify integrity: $expectedHmac = hash_hmac('sha256', $serializedData, SECRET_KEY); if (!hash_equals($expectedHmac, $providedHmac)) { throw new SecurityException('Tamper detected'); }, ensuring serialized data wasn't modified even if attackers know allowed classes. Test allowlist effectiveness: use PHPGGC or similar tools attempting to craft exploits with allowed classes, perform code review of allowed classes searching for exploitable patterns, and conduct penetration testing specifically targeting serialization with restricted classes. Monitor for exploitation attempts: log deserialize operations with unexpected classes detected, alert when __PHP_Incomplete_Class appears indicating blocked deserialization attempt, and track anomalies in deserialized data patterns. Limitations: allowed_classes only available PHP 7.0+, older versions require JSON migration or third-party solutions, and some complex frameworks may break with class restrictions requiring careful testing.

3

Validate and Sanitize All Input Before Deserialization

Never pass user-controlled input directly to unserialize(); implement comprehensive validation ensuring only legitimate serialized data undergoes deserialization. Input source validation: never unserialize data directly from $_GET, $_POST, $_COOKIE, $_REQUEST, or HTTP headers without validation, retrieve serialized data only from trusted server-side sources (database, file system, secure cache) after authentication/authorization checks, and validate user session/authentication before retrieving serialized data associated with that user. Format validation checking serialized structure: serialized PHP format follows specific patterns starting with data type indicators (s:length, i:value, O:class, a:count), validate serialized string matches expected format: if (!preg_match('/^[a-z]:\d+:/', $serialized)) { throw new InvalidArgumentException(); }, check for suspicious patterns: reject strings containing unexpected class names, excessively nested structures indicating attack payloads, or malformed serialization syntax attempting parser confusion. Length validation: enforce maximum serialized data length preventing denial-of-service and overly complex object graphs: if (strlen($serialized) > MAX_SERIALIZED_LENGTH) { throw new SecurityException('Data too large'); }, adjust limits based on legitimate use cases (user preferences: few hundred bytes, cached objects: tens of kilobytes). Class name allowlist extraction and validation: parse serialized string extracting class names: preg_match_all('/O:\d+:"([^"]+)"/', $serialized, $matches), compare extracted classes against allowlist ensuring only permitted classes present: $classNames = $matches[1]; foreach ($classNames as $class) { if (!in_array($class, $allowedClasses, true)) { throw new SecurityException('Disallowed class'); } }, reject deserialization if any class not explicitly allowed. Structural validation: limit object nesting depth preventing deeply nested POP chains, restrict number of properties per object preventing bloated attack payloads, and validate array sizes ensuring reasonable bounds. Sanitize before deserialize: if using allowed_classes, pre-filter serialized string removing references to disallowed classes: $filtered = preg_replace('/O:\d+:"(?!' . implode('|', $allowed) . '")[^"]+"/', 'N;', $serialized) replacing disallowed objects with null, though this approach risky as regex parsing serialization format may miss sophisticated bypasses. Post-deserialization validation: check deserialized object type: instanceof ExpectedClass, validate object properties: $obj->validateProperties() method verifying property values match constraints, recursively validate nested objects ensuring complete object graph is safe, and sanitize any strings or user data within deserialized structure. Implement value range validation: for numeric properties check ranges (user age: 0-150, quantity: >0), for string properties validate format (email, URL, alphanumeric), and for array properties validate keys and values. Rate limiting: limit deserialization operations per user/session preventing brute-force exploitation attempts, track failed deserialization attempts implementing temporary lockouts after threshold, and monitor for patterns indicating scanning or exploitation. Logging and monitoring: log all deserialization operations including data source, user, timestamp, and success/failure, alert on validation failures indicating potential attacks, and analyze patterns detecting coordinated exploitation attempts. Comprehensive validation requires: input validation before deserialization, allowed_classes restricting instantiation during deserialization, post-deserialization type/value validation, and HMAC verification ensuring data integrity. No single control sufficient—defense-in-depth essential.

4

Use Cryptographic Signatures to Verify Serialized Data Integrity

Implement HMAC-based integrity verification ensuring serialized data hasn't been tampered with, preventing attackers from injecting malicious serialized objects even with database or cache access. Generate HMAC signature when serializing: $serialized = serialize($data); $signature = hash_hmac('sha256', $serialized, SECRET_SIGNING_KEY); store both serialized data and signature: store in database as separate columns (data TEXT, signature VARCHAR(64)), concatenate for transmission: $payload = base64_encode($serialized) . '.' . $signature, or use dedicated structure: json_encode(['data' => base64_encode($serialized), 'sig' => $signature]). Verify signature before deserializing: retrieve stored signature: [$serializedData, $storedSignature] = explode('.', $payload, 2), compute expected signature: $expectedSignature = hash_hmac('sha256', $serializedData, SECRET_SIGNING_KEY), compare using timing-safe comparison: if (!hash_equals($expectedSignature, $storedSignature)) { throw new SecurityException('Signature verification failed'); }, only unserialize if signatures match: $data = unserialize($serializedData). Use secure signing key: generate cryptographically random key: $signingKey = random_bytes(32), store in secure configuration management (environment variable, secrets manager) not in code: SECRET_SIGNING_KEY from environment, rotate keys periodically (quarterly/annual), and maintain key rotation supporting old and new keys during transition. Key management best practices: store keys separate from data (different AWS Secrets Manager secrets, separate Vault paths), use separate keys for different purposes (session signing key, cache signing key, persistent storage key), implement key access audit logging tracking who accessed signing keys, and use HSM (Hardware Security Module) for highest security production environments. Implement signature versioning: include version in signature enabling key rotation: $signature = 'v1.' . hash_hmac('sha256', $serialized, $keyV1) or $signature = 'v2.' . hash_hmac('sha256', $serialized, $keyV2), handle multiple versions: if (strpos($signature, 'v1.') === 0) { /* use keyV1 */ } elseif (strpos($signature, 'v2.') === 0) { /* use keyV2 */ }, support gradual migration: accept old version signatures temporarily, re-sign with new version when writing, and eventually stop accepting old signatures after migration complete. Handle signature failures securely: log signature verification failures with context (user ID, data source, timestamp), invalidate session if session signature fails, alert security team on repeated failures indicating attack, and never fall back to unverified deserialization on signature failure. Additional security measures: include timestamp in signed data preventing replay attacks: $data = ['timestamp' => time(), 'payload' => $originalData]; $serialized = serialize($data); verify timestamp during deserialization: if (abs($data['timestamp'] - time()) > 3600) { throw new SecurityException('Expired'); }, include user context preventing cross-user data use: $dataWithContext = ['user_id' => $userId, 'data' => $data]; preventing attacker from using victim's signed serialized data in different context. Encrypt in addition to sign: signing prevents tampering but doesn't hide contents; encrypt sensitive serialized data: $encrypted = openssl_encrypt($serialized, 'aes-256-gcm', $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag); $payload = base64_encode($iv . $tag . $encrypted); GCM mode provides authenticated encryption (AEAD) combining confidentiality and integrity. Testing: attempt to modify serialized data verifying signature verification detects changes, test key rotation ensuring seamless transition without breaking existing data, and penetration test attempting to bypass signature verification.

5

Consider Using Safer Alternatives like MessagePack or Protocol Buffers

Migrate from PHP serialize/unserialize to modern serialization formats designed for security and efficiency: MessagePack, Protocol Buffers, FlatBuffers, or CBOR. MessagePack provides binary JSON-like format: install via Composer: composer require msgpack/msgpack, encode data: $packed = msgpack_pack($data) serializing arrays and scalars without object instantiation, decode: $data = msgpack_unpack($packed) returning only arrays/scalars never objects eliminating object injection risk. MessagePack advantages: language-agnostic enabling cross-platform compatibility, faster than JSON and more compact binary format, no object instantiation preventing deserialization attacks, and mature implementations in most languages (Python, Ruby, JavaScript, Java, Go). Protocol Buffers (protobuf) uses schema-based serialization: define schema in .proto file: message UserData { int32 id = 1; string name = 2; string email = 3; }, generate PHP classes: protoc --php_out=. user.proto, serialize: $user = new UserData(); $user->setId(1); $serialized = $user->serializeToString();, deserialize: $user = new UserData(); $user->mergeFromString($serialized);. Protocol Buffers advantages: strict schema enforcement preventing unexpected data types, no arbitrary object instantiation, backward/forward compatibility with versioned schemas, efficient binary format, and strong typing with compile-time checks. FlatBuffers optimizes for zero-copy deserialization: define schema: table UserData { id: int; name: string; email: string; }, compile schema: flatc --php user.fbs, use zero-copy access: $buffer = $bytes; $user = UserData::getRootAsUserData($buffer); accessing data without parsing entire structure. FlatBuffers advantages: extremely fast deserialization, no parsing step (direct memory access), suitable for high-performance applications, and mobile-friendly (developed by Google for games). CBOR (Concise Binary Object Representation) similar to MessagePack: use cbor-php library: composer require2\cbor-php, encode: $encoded = Cbor::encode($data), decode: $data = Cbor::decode($encoded). CBOR advantages: IETF standard (RFC 7049/8949), supports more data types than JSON, self-describing format, and security-focused design. Implementation strategy: evaluate alternatives based on requirements: MessagePack for drop-in JSON replacement, Protocol Buffers for complex cross-service communication requiring strict schemas, FlatBuffers for performance-critical real-time systems, CBOR for IoT or embedded systems, start with MessagePack as easiest migration from serialize/unserialize. Migration approach: implement dual-mode support during transition: try alternative format, fall back to unserialize for legacy data, implement background job migrating old data to new format, and eventually remove unserialize support after complete migration. Consider framework integration: Symfony: use custom serializer: Symfony\Component\Serializer\Encoder\MessagePackEncoder, Laravel: create custom caster for Eloquent models, and implement transparent migration in ORM hydration/serialization. Performance testing: benchmark new format vs serialize/unserialize measuring serialization/deserialization time and payload size, test with realistic production data volumes, and ensure performance meets requirements before full migration. Backward compatibility: maintain ability to read old serialized data during transition using fallback reading strategy, gradually migrate data re-serializing with new format on write, and monitor migration progress tracking percentage of data still using old format. Schema management: for Protocol Buffers/FlatBuffers maintain .proto/.fbs files in version control, document schema evolution strategy (adding fields, removing fields, type changes), test compatibility between schema versions, and use schema registries for complex distributed systems. Training and documentation: document migration rationale and benefits, provide code examples for common use cases, train developers on new serialization APIs, and update coding standards requiring new format for all new code.

6

Remove or Secure Dangerous Magic Methods in Classes

Audit application codebase identifying and removing dangerous magic method implementations that enable POP chain exploitation, or secure them against malicious property values. Audit magic methods in all classes: search codebase for __destruct: grep -r '__destruct' --include='*.php', identify __wakeup, __toString, __call, __get, __set, __invoke implementations, and document purpose and safety of each magic method. Eliminate unnecessary magic methods: remove __destruct if not essential: many __destruct implementations only cleanup that PHP garbage collector handles automatically, remove __wakeup if no post-deserialization initialization needed, and simplify __toString returning static/safe strings avoiding complex logic. Secure __destruct implementations: never perform destructive operations (file deletion, database modifications) in __destruct based on object properties: change class FileLogger { private $file; public function __destruct() { unlink($this->file); } } to explicit cleanup methods: public function cleanup() { if ($this->canDelete()) unlink($this->file); } requiring explicit invocation with safety checks. Validate property values in magic methods: at start of __destruct: public function __destruct() { if (!$this->isValid()) return; /* cleanup */ }, implement validation: private function isValid() { return is_string($this->file) && strpos($this->file, '/tmp/') === 0; }, and whitelist allowed values using constants/enums not free-form strings. Redesign classes avoiding magic methods: use explicit initialization methods: initialize() called manually instead of __wakeup automatic invocation, implement explicit cleanup: try { /* use object */ } finally { $obj->close(); } instead of __destruct automatic cleanup, use explicit string conversion: getAsString() instead of __toString, and prefer explicit method dispatch over __call magic routing. Restrict property visibility: make dangerous properties private/protected preventing external modification: private $filename; instead of public preventing attackers from directly setting values during unserialize, implement validation in setters: public function setFilename($name) { if (!$this->isValidFilename($name)) throw new InvalidArgumentException(); $this->filename = $name; }, and use __sleep()/__wakeup() controlling serialization: public function __sleep() { return ['safeProperty1', 'safeProperty2']; } excluding dangerous properties from serialization. Implement Serializable interface: class SafeClass implements Serializable { public function serialize() { /* custom safe serialization */ } public function unserialize($data) { /* validate before setting properties */ } } providing complete control over serialization/deserialization. Use JsonSerializable for safer serialization: implement JsonSerializable interface: public function jsonSerialize() { return ['id' => $this->id, 'name' => $this->name]; } controlling exactly what data serializes, migrate to JSON serialization replacing serialize/unserialize, and prevent object graph serialization by returning only scalars/arrays. Code review process: require security review for any new magic method implementations, document business justification for magic methods in comments/docblocks, and reject PRs adding dangerous __destruct/__wakeup patterns without compelling reason. Security testing: use PHPGGC testing if application classes exploitable: ./phpggc -l checking available gadget chains for your dependencies, attempt crafting POP chains using application classes, and fix any discovered exploitation paths. Framework-level controls: configure Symfony security: framework.serializer.default_context.enable_max_depth: 3 limiting nesting, implement event listeners intercepting serialization adding validation, and use Symfony\Component\Security\Core\User\UserInterface requiring authentication for deserialization. Dependency audit: audit third-party packages for dangerous gadgets, monitor security advisories for new POP chains discovered in dependencies, and consider alternatives to dependencies with known serialization issues. Document safe patterns: maintain secure coding guidelines with magic method best practices, provide code review checklist for reviewing magic method implementations, and create training materials on deserialization security.

Detect This Vulnerability in Your Code

Sourcery automatically identifies php unserialize() deserialization vulnerability and many other security issues in your codebase.