The Exploit
An unauthenticated attacker submits a POST request to the WooCommerce order export endpoint with a malicious serialized PHP object in the order data field when the "Try to convert serialized values" option is enabled on the target site. Upon processing, the plugin deserializes the object without restricting class instantiation, triggering a PHP gadget chain that deletes arbitrary files (commonly wp-config.php to achieve RCE).
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
action=woe_export&security=<nonce>&export_type=csv&order_fields=<serialized_payload>
Where <serialized_payload> is a crafted serialized object generated by a gadget chain tool (e.g., PHPGGC) targeting WordPress or a bundled library. The attacker observes the export endpoint processes the malicious object without error. Within seconds, if a suitable POP chain exists in the WordPress installation, filesystem modifications occur — most critically, wp-config.php is deleted, causing the site to enter installation mode and allowing the attacker to reconfigure the database credentials and inject malicious code.
What the Patch Did
Before
if ( $options['convert_serialized_values'] ) {
$arr = maybe_unserialize( $row[ $field ] );
After
if ( $options['convert_serialized_values'] AND is_serialized($row[ $field ]) ) {
$arr = unserialize( trim($row[ $field ]), ['allowed_classes' => false] );
The patch added two critical security controls: (1) a pre-flight check using is_serialized() to validate that the value is actually serialized data before attempting deserialization, preventing accidental or malicious triggering of unserialize on non-serialized input, and (2) the ['allowed_classes' => false] option to the unserialize() call, which is a PHP language-level control that restricts deserialization to primitive types (arrays, strings, integers, booleans, floats, NULL) and blocks instantiation of any PHP objects. This prevents the instantiation of gadget chains entirely.
Root Cause
CWE-502: Deserialization of Untrusted Data. The vulnerable code path accepts order field data (the $row[ $field ] variable populated from order export rows) and unconditionally passes it to maybe_unserialize() when the "Try to convert serialized values" option is enabled. This option is a user-configurable setting that an attacker can assume is enabled on some installations. The maybe_unserialize() function performs no class allowlisting, permitting any PHP object to be instantiated if the input is valid serialized data. An unauthenticated attacker can influence order data at export time through maliciously crafted database records (if they can inject into the database via another vector) or by supplying data directly to the export request if the plugin does not validate request source. The untrusted serialized string crosses the trust boundary and is deserialized without restriction, allowing object injection.
Why It Works
The load-bearing line is ['allowed_classes' => false]. If this option is removed and the code reverts to unserialize( trim($row[ $field ]) ) without the allowlist, the bug is fully re-exploitable — an attacker can still supply a serialized gadget chain object that instantiates and executes destructive code. The is_serialized() check is a secondary defence that prevents obvious non-serialized input from triggering unserialize warnings, but it does not stop a well-formed serialized payload. The trim() wrapper is hygiene to remove whitespace that might trip the parser. The critical insight is that allowing object instantiation during deserialization is inherently unsafe in the presence of a POP chain; the only safe default is to forbid all classes via the language API, forcing the developer to explicitly enumerate allowed classes if deserialization of objects is actually necessary (which it is not for export use cases).
Hardening Checklist
- Never call
unserialize()on user-controlled data without['allowed_classes' => false]as a minimum. Better: avoid unserialize entirely for user input and usejson_decode()with a schema validator instead. - Validate data shape and content before deserialization. Use
is_serialized()to confirm the input is actually serialized, and log unexpected data to detect injection attempts. - Use WordPress sanitization APIs on all input sources. Apply
sanitize_text_field()orwp_kses_post()to POST/GET parameters and database-sourced data before processing, even if that data will later be unserialized. - Implement capability checks on export endpoints. Verify
current_user_can('manage_woocommerce_orders')or similar before allowing export requests, restricting the attack surface to authenticated users with explicit permission. - Audit installed plugins for gadget chains. Run PHPGGC or similar tooling against the WordPress installation to identify available POP chains, then document which classes are dangerous and forbidden in unserialize contexts.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10828