The Exploit
An unauthenticated attacker crafts a serialized PHP object and stores it in a Contact Form 7 entry via the plugin's form submission mechanism, then retrieves it through the get_lead_detail function to trigger object instantiation.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
action=vxcf_form_submit&form_id=1&field_name=your_name&your_name=O:8:"stdClass":1:{s:4:"name";s:5:"value";}&field_name=your_email&[email protected]
When the attacker later requests get_lead_detail(entry_id), the serialized object in $lead['detail'] is passed directly to maybe_unserialize() without validation. If a suitable POP chain exists in Contact Form 7 (or another active plugin), the deserialized object's __destruct() or __wakeup() methods execute. The attacker observes silent code execution or, in this case, arbitrary file deletion (wp-config.php) leading to denial of service. No error is raised; the attack succeeds in-process.
What the Patch Did
Before
}else if(is_serialized($string)){
$string=maybe_unserialize($string);
}
$value='';
if(isset($lead_field['value'])){
$value=maybe_unserialize($lead_field['value']);
}
$field_label=maybe_unserialize($lead['detail'][$field_name]);
After
}
/*else if(is_serialized($string)){
$string=maybe_unserialize($string);
} */
$value=$lead_field['value'];
if( !empty($value)){
$value=vxcf_form::maybe_unserialize($value,$lead);
}
$field_label=vxcf_form::maybe_unserialize($lead['detail'][$field_name],$lead);
The patch introduces three layers of defense. First, it removes raw calls to the WordPress maybe_unserialize() function, which blindly deserializes any serialized string without validation. Second, it introduces a custom wrapper method vxcf_form::maybe_unserialize() that accepts a $lead context parameter, allowing context-aware validation before deserialization occurs. Third, it replaces the loose isset() check with !empty(), rejecting null, false, and zero values that could be weaponized. The load-bearing change is the substitution of untrusted deserialization with a gated custom method that can enforce application-level constraints.
Root Cause
CWE-502: Deserialization of Untrusted Data. The vulnerability originates in the request parameter your_name (or any form field) submitted to wp-admin/admin-ajax.php?action=vxcf_form_submit. This user-controlled value is stored in the database as-is, including serialized PHP objects. When get_lead_detail() or the template rendering functions (leads.php, leads-table.php, view.php, data.php) retrieve the lead entry, they pass $lead['detail'][$field_name] directly to maybe_unserialize(). No sanitization, signature verification, or object-type allowlist is performed. The untrusted boundary—the form submission input—crosses directly into the deserialization sink without validation. An attacker who can craft serialized objects and who knows a POP chain in Contact Form 7 or a coupled plugin can achieve arbitrary code execution or file deletion.
Why It Works
The single load-bearing line is the introduction of the custom vxcf_form::maybe_unserialize($value, $lead) method. If this wrapper were removed and the code reverted to raw maybe_unserialize() calls, the vulnerability would remain exploitable because maybe_unserialize() is a permissive WordPress utility that will deserialize any valid serialized string, including malicious objects. The engineer added the $lead context parameter to enable timestamp-based validation (checking strtotime($lead['created']) < 1754290480) and likely other checks that reduce the trust window for deserialization. The engineer also added the !empty($value) pre-check to reject obviously invalid or null payloads before they reach the deserialization function. This defense-in-depth approach means an attacker must not only craft a malicious object but also either forge or compromise a lead entry with a timestamp in the accepted range. Together, these layers make exploitation significantly harder than the original code, where any stored value would be deserialized on retrieval.
Hardening Checklist
-
Avoid
maybe_unserialize()on untrusted data. Usejson_decode()for structured form data instead. If serialization is unavoidable, store a cryptographic HMAC alongside the serialized value and verify it on retrieval usinghash_hmac()before deserializing. -
Implement an object-type allowlist. If deserialization is necessary, use a custom unserialize function that only permits instantiation of safe classes (e.g.,
stdClass,ArrayObject) and rejects all others. WordPress'smaybe_unserialize()has no such guard. -
Store form submissions as JSON or sanitized key-value pairs. Avoid storing serialized PHP objects in the database altogether. Use
wp_json_encode()andjson_decode()for all form data interchange. -
Apply
sanitize_text_field()to all form field values immediately upon submission, before storing them. This converts any serialized payload into escaped plaintext and eliminates the attack vector at the entry point. -
Use prepared statements and stored procedures to isolate data from code. Ensure the database layer does not interpret stored form data as executable code by binding values as parameters, not string concatenation.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-7384