VNX-PHP-006 – PHP Object Injection via unserialize()
Overview
This rule flags calls to unserialize() and maybe_unserialize() (a WordPress helper) where the input comes from user-controlled superglobals ($_GET, $_POST, $_REQUEST, $_COOKIE), and also flags any bare unserialize() call in the codebase as a high-risk pattern worth reviewing. PHP’s serialization format can encode arbitrary object graphs; when unserialize() processes attacker-supplied data, it instantiates PHP objects and calls their __wakeup() and __destruct() magic methods during the deserialization process — before any application logic has a chance to validate the result. This maps to CWE-502: Deserialization of Untrusted Data.
Severity: Critical | CWE: CWE-502 – Deserialization of Untrusted Data
Why This Matters
PHP Object Injection (POI) is a code execution primitive that does not require a file write or a file inclusion vulnerability. An attacker constructs a serialized string that, when deserialized, instantiates objects from classes already loaded in the application. By chaining together classes whose __wakeup(), __destruct(), __toString(), or other magic methods perform dangerous operations — writing files, executing commands, making HTTP requests — the attacker assembles a “gadget chain” that executes arbitrary code when the string is deserialized.
Ready-made gadget chains exist for virtually every major PHP framework and CMS. Tools like phpggc (PHP Generic Gadget Chains) automate the generation of exploit payloads targeting Symfony, Laravel, WordPress, Zend/Laminas, Yii, Magento, and Drupal. If your application uses any of these frameworks and calls unserialize() on untrusted data, it is almost certainly exploitable without any further preconditions.
What Gets Flagged
The rule matches lines where unserialize() or maybe_unserialize() receive direct superglobal input, and also matches any unserialize( call as a broader pattern.
// FLAGGED: unserialize with GET parameter — direct POI vector
$data = unserialize($_GET['data']);
// FLAGGED: unserialize with POST data
$obj = unserialize($_POST['payload']);
// FLAGGED: cookie-based deserialization — common attack vector
$prefs = unserialize($_COOKIE['preferences']);
// FLAGGED: WordPress maybe_unserialize with user input
$value = maybe_unserialize($_REQUEST['option']);
// FLAGGED: bare unserialize — any call is worth reviewing
$result = unserialize($data_from_db);
Remediation
- Replace
unserialize()withjson_decode()for data exchange formats. JSON cannot represent PHP objects or trigger magic methods — it decodes to arrays and scalars only. This eliminates the object injection surface entirely:
// SAFE: JSON decoding — no object instantiation, no magic methods
$prefs = json_decode($_COOKIE['preferences'], true);
if (!is_array($prefs)) {
$prefs = [];
}
- If you must use
unserialize(), setallowed_classestofalseor an explicit whitelist. Since PHP 7.0,unserialize()accepts anoptionsarray. Settingallowed_classestofalseprevents any class from being instantiated during deserialization, neutralizing gadget chain attacks:
// SAFE: allowed_classes restricts which objects can be created
$data = unserialize($input, ['allowed_classes' => false]);
// SAFE: or allow only specific, known-safe classes
$data = unserialize($input, ['allowed_classes' => ['MyValueObject', 'UserPreferences']]);
- Sign or authenticate serialized data before trusting it. If you serialize data and store it in a cookie, session, or database field that could be tampered with, include an HMAC signature computed with a server-side secret. Verify the signature before deserializing:
// SAFE: HMAC-authenticated serialization (sign before store, verify before load)
function serialize_signed(mixed $data, string $key): string {
$payload = base64_encode(serialize($data));
$sig = hash_hmac('sha256', $payload, $key);
return $sig . '.' . $payload;
}
function unserialize_verified(string $token, string $key): mixed {
[$sig, $payload] = explode('.', $token, 2) + ['', ''];
$expected = hash_hmac('sha256', $payload, $key);
if (!hash_equals($expected, $sig)) {
throw new \RuntimeException('Invalid signature');
}
return unserialize(base64_decode($payload), ['allowed_classes' => false]);
}
Audit every
unserialize()call in the codebase, including indirect uses through ORM hydrators, cache libraries, and session handlers. Many PHP session handlers serialize and deserialize session data automatically — ensure the session storage backend is not accessible to untrusted parties.Keep framework versions current. Framework maintainers regularly remove unsafe magic methods from classes to neutralize known gadget chains. An up-to-date Composer
composer.lockcombined withallowed_classesrestrictions is the strongest combination.