VNX-PHP-006: PHP object injection via unserialize
Detect unserialize() and maybe_unserialize() on a request value (superglobals, php://input, request accessors, also wrapped in base64_decode() and similar) without an allowed_classes restriction.
| Field | Value |
|---|---|
| Rule ID | VNX-PHP-006 |
| Severity | Critical |
| SARIF level | error |
| Kind | sast |
| Languages | PHP |
| CWE | CWE-502 |
| CAPEC | CAPEC-586 |
| ATT&CK | T1059 |
| CWSS vector | CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W |
| Tags | deserialization object-injection php |
| Source | internal/sast/rules/vnx-php-006.rego |
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), inline or through a variable assigned from one, unless allowed_classes restricts object creation. 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.
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
A call whose deserialised value is a request value, inline (optionally wrapped in a decoding function) or through a variable assigned from one a few lines above, with no allowed_classes option that disables or restricts object creation.
// FLAGGED
$cart = unserialize($_COOKIE["cart"]);
$prefs = unserialize(base64_decode($_COOKIE["prefs"]));
$payload = $_POST["data"];
$obj = unserialize($payload);
$settings = maybe_unserialize( stripslashes( $_POST["settings"] ) );
Not flagged: unserialize($x, ["allowed_classes" => false]) or an explicit class list, inline or through an options variable such as $opts = ["allowed_classes" => false]; a value authenticated with hash_equals() first; a request value used only as a lookup key (unserialize($redis->get("session:" . $_COOKIE["sid"]))); cache, queue and file payloads the application wrote itself; methods named unserialize; json_decode().
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.
References
- CWE-502: Deserialization of Untrusted Data
- CAPEC-586: Object Injection
- OWASP Deserialization Cheat Sheet
- OWASP PHP Security Cheat Sheet
- PHP manual: unserialize() – allowed_classes
- phpggc – PHP Generic Gadget Chains
- MITRE ATT&CK T1059 – Command and Scripting Interpreter
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-php-006
// nosec vnx-php-006
Or record an ignore rule: vulnetix ignore add --rule vnx-php-006 --reason "...". See Suppressing findings.