CVE-2026-3459 WordPress Drag and Drop Multiple File Upload – Contact Form 7 Plugin <= 1.3.9.5 is vulnerable to a high priority Arbitrary File Upload

image

Overview

Published: 2026-03-05 CVE-ID: CVE-2026-3459 CVSS: 8.1 High Affected Plugin: Drag and Drop Multiple File Upload – Contact Form 7 Plugin Affected Versions: <= 1.3.9.5 Vulnerability Type: Arbitrary File Upload CWE: CWE-434 Unrestricted Upload of File with Dangerous Type

Description

The Drag and Drop Multiple File Upload - Contact Form 7 plugin for WordPress is vulnerable to arbitrary file uploads due to insufficient file type validation in the 'dnd_upload_cf7_upload' function in versions up to, and including, 1.3.7.3. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site’s server which may make remote code execution possible. This can be exploited if the form includes a multiple file upload field with ‘*’ as the accepted file type.

Patch And Commit Analysis

image

According to the changelog, this vulnerability was patched in version 1.3.9.6. In earlier releases (≤ 1.3.9.5), attackers could exploit the flaw to upload and execute arbitrary files such as php5, php8, and similar payloads. To analyze the fix, I will compare the patch diff between version 1.3.9.5 (vulnerable) and 1.3.9.6 (patched).

Since this is a File Upload vulnerability, the analysis will focus specifically on that aspect. I will omit code segments that are not directly relevant to the exploit or the patch.

Root Cause

When a CF7 form is configured with filetypes=* (wildcard), the dnd_upload_cf7_upload() function fails to enforce proper restrictions on dangerous file types, allowing unauthenticated attackers to upload PHP webshells and achieve Remote Code Execution.

Wildcard Blacklist — Insufficient Extension Blocking

image

Impact: The vulnerable version only blocks phar and svg, leaving .php, .php5, .php7, .php8, .phtml and many other executable extensions completely unblocked. The patch also fixes wp_check_filetype() to use the sanitized $filename instead of the raw $file[’name’], preventing extension spoofing via crafted filenames.

Global Blacklist — dnd_cf7_not_allowed_ext()

image

// Vulnerable
return array(
    'svg', 'phar', 'php', 'php3', 'php4', 'phtml', 'exe', ...
    // missing: php5, php7, php8, pht, html, xhtml, shtml, mhtml, dhtml
);

// Patched
return array(
    'html', 'svg', 'phar',
    'php', 'php3', 'php4', 'pht', 'php5', 'php7', 'php8',  // ← added
    'xhtml', 'shtml', 'mhtml', 'dhtml',                      // ← added
    'phtml', 'exe', ...
);

Impact: The global blacklist used across all validation paths was missing multiple dangerous PHP variants and HTML-based script extensions that could be used for server-side execution depending on server configuration.

Filename Sanitization — Missing sanitize_file_name()

image

// VULNERABLE
$filename = wp_basename( $file['name'] );
$filename = wpcf7_canonicalize( $filename, 'as-is' );
// → goes directly into validation with dirty filename

// PATCHED
$filename = wp_basename( $file['name'] );
$filename = wpcf7_canonicalize( $filename, 'as-is' );
$filename = sanitize_file_name( $filename ); // ← added

Impact: Without sanitize_file_name(), specially crafted filenames could slip through extension validation checks. The patch ensures the filename is fully sanitized before any validation occurs.

Processing Order — Extension Extraction vs Unique Filename

image

// VULNERABLE — extension extracted BEFORE sanitization
$filename  = wpcf7_canonicalize( $filename, 'as-is' );
$extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // dirty filename
$filename  = wp_unique_filename( $path['upload_dir'], $filename );

// PATCHED — sanitize first, then unique check, then extract extension
$filename  = wpcf7_canonicalize( $filename, 'as-is' );
$filename  = sanitize_file_name( $filename );
$filename  = wp_unique_filename( $path['upload_dir'], $filename );
$extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // clean filename

Impact: In the vulnerable version, $extension is extracted from an unsanitized filename, meaning extension-based validation could be tricked via crafted input. The patch reorders the pipeline to: sanitize → unique check → extract extension → validate.

Emoji Bypass — wpcf7_antiscript_file_name() Skipped

image

Impact: A filename containing emoji characters causes mb_check_encoding($string, ‘ASCII’) to return false, which skips the wpcf7_antiscript_file_name() call entirely. An attacker could craft a filename like shell🔥.php to bypass the antiscript sanitization layer.

Folder Isolation — Architecture Change

image

// VULNERABLE — reads unique ID from cookie server-side
function dnd_cf7_get_unique_id() {
    if ( isset( $_COOKIE['wpcf7_guest_user_id'] ) ) {
        return $_COOKIE['wpcf7_guest_user_id'];
    }
}

// Called in upload flow:
$path = dnd_get_upload_dir( true );
// → dnd_cf7_get_unique_id() called internally

// PATCHED — UUID generated client-side via JS, sent as POST param
// dnd_cf7_get_unique_id() is now dead code, no longer called in upload flow

$folder = isset( $_POST['upload_folder'] ) ? sanitize_text_field( $_POST['upload_folder'] ) : null;
$path   = dnd_get_upload_dir( $folder );
// PATCHED — JS generates UUID client-side
function dnd_cf7_generateUUIDv4() {
    const bytes = new Uint8Array(16);
    crypto.getRandomValues(bytes);
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    bytes[8] = (bytes[8] & 0x3f) | 0x80;
    const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
    return hex.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, '$1-$2-$3-$4-$5');
}
document.cookie = 'wpcf7_guest_user_id=' + dnd_cf7_generateUUIDv4() + '; samesite=Lax';

Impact: The vulnerable version relies on server-side cookie reading to isolate upload folders per user. The patched version fully refactors this — the client generates a cryptographically random UUID via crypto.getRandomValues() and passes it as a POST parameter. This makes the upload folder path unpredictable, significantly raising the bar for an attacker trying to locate an uploaded webshell.

Root Cause Analysis (From source to sink)

Entry Point

The entry point of all uploading processes is located at dnd_upload_cf7_upload():

function dnd_upload_cf7_upload() {
    $cf7_id          = sanitize_text_field( (int)$_POST['form_id'] );
    $cf7_upload_name = sanitize_text_field( $_POST['upload_name'] );
    $allowed_types   = dnd_cf7_get_option( $cf7_id, 'filetypes' );
    $size_limit      = dnd_cf7_get_option( $cf7_id, 'limit' );
    $blacklist       = dnd_cf7_get_option( $cf7_id, 'blacklist-types' );

    if( ! check_ajax_referer( 'dnd-cf7-security-nonce', 'security', false ) ) {
        wp_send_json_error('The security nonce is invalid or expired.');
    }

Nonce Verification

Before proceeding, the request passes through dnd_wpcf7_nonce_check() to verify the user’s nonce:

function dnd_wpcf7_nonce_check() {
    if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'curl' ) !== false ) {
        wp_send_json_error('Request blocked: cURL access is forbidden.');
    }
    if( ! check_ajax_referer( 'dnd-cf7-security-nonce', false, false ) ){
        wp_send_json_success( wp_create_nonce( "dnd-cf7-security-nonce" ) );
    }
}

Note that the nonce is publicly accessible from the page source, meaning any unauthenticated attacker can retrieve it without needing to be logged in.

File Type Validation — Wildcard Branch

The upload flow then enters file type validation. Two separate checks are performed depending on the supported_type value. When a form is configured with filetypes=*, the wildcard branch is triggered:

if ( $supported_type == '*' ) {
    $file_type       = wp_check_filetype( $file['name'] ); // raw, unsanitized filename
    $not_allowed_ext = array( 'phar', 'svg' );             // insufficient blacklist
    $type_ext        = ( $file_type['ext'] !== false ? strtolower( $file_type['ext'] ) : $extension );

    if ( ! empty( $blacklist_types ) && in_array( $type_ext, $blacklist_types, true ) ) {
        wp_send_json_error( $error_invalid_type );
    } elseif ( in_array( $type_ext, $not_allowed_ext, true ) ) {
        wp_send_json_error( $error_invalid_type );
    }
}

The mime type validation block is bypassed entirely because it only runs when $supported_type != ‘*’:

if( $supported_type && $supported_type != '*' ){
    $validate_mime = apply_filters('dnd_cf7_validate_mime', false );
    if( $validate_mime ){
        // mime check — never reached in wildcard mode
    }
}

Global Blacklist — Insufficient Coverage

The wildcard branch relies on dnd_cf7_not_allowed_ext() as a secondary defense. However, the blacklist is missing several dangerous PHP variants:

function dnd_cf7_not_allowed_ext() {
   return array( 'svg', 'phar', 'php', 'php3','php4','phtml','exe','script', 'app', 'asp', 'bas', 'bat', 'cer', 'cgi', 'chm', 'cmd', 'com', 'cpl', 'crt', 'csh', 'csr', 'dll', 'drv', 'fxp', 'flv', 'hlp', 'hta', 'htaccess', 'htm', 'htpasswd', 'inf', 'ins', 'isp', 'jar', 'js', 'jse', 'jsp', 'ksh', 'lnk', 'mdb', 'mde', 'mdt', 'mdw', 'msc', 'msi', 'msp', 'mst', 'ops', 'pcd', 'pif', 'pl', 'prg', 'ps1', 'ps2', 'py', 'rb', 'reg', 'scr', 'sct', 'sh', 'shb', 'shs', 'sys', 'swf', 'tmp', 'torrent', 'url', 'vb', 'vbe', 'vbs', 'vbscript', 'wsc', 'wsf', 'wsf', 'wsh' );
}

Extensions such as .pht are absent from both the wildcard blacklist [‘phar’, ‘svg’] and the global blacklist, allowing them to pass all validation checks undetected.

Filename Processing — Raw Input

$file_type = wp_check_filetype( $file['name'] ); // raw filename, not sanitized

Attacking Flow

image

POC Proof Of Concept

The upload process calls the /wp-admin/admin-ajax.php endpoint directly via AJAX. Under normal circumstances — when a user submits the CF7 form through the browser — the flow passes through wpcf7_antiscript_file_name(), which renames dangerous file extensions by appending _.txt. However, this protection only runs during CF7’s form submission processing, not during the AJAX upload phase. By sending a crafted request directly to admin-ajax.php via curl without triggering a full form submission, the wpcf7_antiscript_file_name() call is never reached, effectively bypassing this layer of defense entirely :

image

By crafting a direct curl POST request to the AJAX endpoint, the file shell.pht was uploaded successfully. Since .pht is absent from both the wildcard blacklist [‘phar’, ‘svg’] and the global blacklist in dnd_cf7_not_allowed_ext(), it bypasses all validation checks. The server responds with a success message confirming the file was stored on disk:

image

After successfully uploading the malicious file, accessing it directly via the browser confirms Remote Code Execution. By appending ?cmd=id to the file URL, the server executes the command and returns the output:

http://localhost/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/shell.pht?cmd=id

image

The response reveals the web server is running as www-data, confirming full unauthenticated RCE on the target system. An attacker can substitute any system command in place of id to enumerate the environment, read sensitive files, establish a reverse shell, or move laterally within the network.

Impact

This vulnerability allows unauthenticated attackers to achieve Remote Code Execution under the following condition:

  • The target CF7 form must have a file upload field configured with filetypes=*

Once exploited, an attacker gains code execution with the privileges of the web server process (www-data), enabling full server compromise, data exfiltration, and persistent backdoor installation.

Remediation

  • Update to version 1.3.9.6 or later immediately
  • Avoid using wildcard (*) file type configuration in CF7 forms
  • Specify only required extensions explicitly (e.g. jpg|png|pdf)
  • Audit upload directories for any suspicious files