The Exploit
An unauthenticated attacker can read arbitrary files from the server by submitting a form with a file-upload field, injecting path-traversal sequences into the old_files POST parameter, and observing the file contents in the notification email. No prior access or authentication is required; the form merely needs to have a file-upload or image-upload field with entry storage disabled.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=evf_submit_form&form_id=1&everest_forms_1_1=&everest_forms_1_1_old_1=["{\\"value\\":\\"http://target.wordpress.local/wp-content/..\\\\..\\\\..\\\\wp-config.php\\"}"]&everest_forms_1_delete_1=
The attacker's path-traversal payload (..\\..\\..\\wp-config.php) is decoded by the vulnerable regex replacement, which strips everything before wp-content and then concatenates ABSPATH + wp-content/ + the attacker's path. Because no canonicalization occurs, directory boundaries are not enforced. The resolved file path escapes the uploads directory and points to wp-config.php. The plugin attaches this file to the notification email, leaking database credentials and security salts to the attacker's inbox.
Alternatively, if the form processes the request before email delivery, the post-email cleanup routine will call unlink() on the same unvalidated path, deleting the target file and causing denial of service.
What the Patch Did
Before
$uploaded_file = ABSPATH . preg_replace( '/.*wp-content/', 'wp-content', wp_parse_url( $file, PHP_URL_PATH ) );
if ( ! in_array( $uploaded_file, $entry_files ) && file_exists( $uploaded_file ) ) {
$entry_files [] = $uploaded_file;
}
After
$resolved = $this->resolve_uploads_file_from_url( $file );
if ( false !== $resolved && ! in_array( $resolved, $entry_files, true ) ) {
$entry_files[] = $resolved;
}
The patch replaced a naive string-concatenation strategy with a dedicated validation function, resolve_uploads_file_from_url(), which performs canonicalization using realpath(), verifies the resolved path stays within the WordPress uploads directory using strpos() boundary checks, and confirms the file exists before returning it. The control is directory confinement — the fix ensures that any attacker-supplied URL, regardless of path-traversal encoding, is resolved to its true filesystem location and then validated against the uploads basedir boundary.
Root Cause
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
The vulnerability flows from two trust boundaries crossed without validation:
-
Untrusted old_files parameter: The
$_POST[ 'everest_forms_' . $form_id . '_old_' . $field_id ]POST parameter accepts JSON-encoded file objects with avaluefield containing URLs. The plugin assumes these URLs are legitimate server-side upload state, but an attacker can inject arbitrary URLs pointing to any file on the filesystem. -
Unsafe path resolution: The code converts the attacker-supplied URL into a filesystem path using only a regex replacement (
preg_replace( '/.*wp-content/', 'wp-content', ... )). This regex strips everything up to and including the first occurrence ofwp-content, then the string is prepended withABSPATH. An attacker can encode path-traversal sequences (e.g.,../../../wp-config.php) in the URL; the regex does not canonicalize them, so they remain in the final path and allow directory escape.
The second vulnerability in the patch diff — improper input validation — compounds this by removing type checks on $file_data['value']. The fixed version adds is_string() assertions before attempting path resolution.
Why It Works
The load-bearing line is $resolved = $this->resolve_uploads_file_from_url( $file ), which replaces the entire old path-resolution logic. If you removed it and kept the original regex approach, the bug is still exploitable: an attacker's ../../wp-config.php payload would still reach unlink().
The function itself performs three defenses:
realpath()canonicalization: Resolves all..and symbolic-link sequences to an absolute path, collapsing the attacker's encoding tricks into a single true path.- Basedir boundary check: Uses
strpos()to confirm the canonical path starts with the uploads basedir; if it does not, the function returnsfalse, blocking the attachment and deletion. - File existence check: Only returns the path if the file actually exists, preventing attackers from discovering non-existent files through error messages.
The engineer added the others — is_string() type checks, parameter validation, esc_url_raw() sanitization — to prevent subtle bypasses: an attacker who passes a malformed value field (null, array, object) could cause type confusion or fatal errors that skip the path check. Together, they form a defense-in-depth against both direct traversal and input-smuggling variants.
Hardening Checklist
- Use
wp_normalize_path()andrealpath()for all user-controlled file paths: Before any filesystem operation (stat, read, unlink), resolve the path to its canonical form and verify it stays within an expected directory usingstrpos()prefix matching. - Sanitize URL inputs with
esc_url_raw()before parsing: Even if you do not execute a URL as a link, parsing untrusted URLs withwp_parse_url()can fail silently or return misleading components; always sanitize first. - Validate file locations against a whitelist of allowed directories: Maintain an allow-list of directories (e.g.,
wp_get_upload_dir()['basedir']) and reject any resolved path that does not fall within one of them. - Add type assertions (
is_string(),is_array()) on all unserialized and POST-sourced data: JSON decoding can return unexpected types; check the type before accessing fields or passing to functions expecting a specific type. - Prefer a dedicated upload/file-retrieval API over manual path construction: If your plugin re-implements file handling, audit it against
wp_safe_remote_get()andwp_get_upload_dir()behavior to catch trust-boundary violations.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-5478