1. Overview
A vulnerability exists in WPMU DEV’s Forminator Forms plugin for WordPress (600,000+ active installations) that allows unauthenticated attackers to upload arbitrary PHP files to the web server. The form processing logic trusts a client-supplied return key in Select field POST data, injecting attacker-controlled upload field configuration into the internal field processing array. Combined with a separate weakness in the file extension blocklist that uses exact-key matching, an attacker can bypass dangerous-extension filtering and upload executable PHP files. Successful exploitation achieves remote code execution under the web server’s user context. The vulnerability affects all Forminator Forms versions through 1.56.1 and was patched in version 1.56.2, released July 31, 2026.
2. Vulnerability Type
| Field | Value |
|---|---|
| Primary CWE | CWE-434: Unrestricted Upload of File with Dangerous Type |
| Related CWE | CWE-20: Improper Input Validation |
3. Severity
CVSS 3.1 (from Wordfence Advisory)
| Field | Value |
|---|---|
| Score | 9.8 (Critical) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
Our Assessment (CVSS 4.0)
| Metric Group | Metric | Value |
|---|---|---|
| Base — Exploitability | Attack Vector (AV) | Network |
| Attack Complexity (AC) | Low | |
| Attack Requirements (AT) | Present | |
| Privileges Required (PR) | None | |
| User Interaction (UI) | None | |
| Base — Vulnerable System | Confidentiality (VC) | High |
| Integrity (VI) | High | |
| Availability (VA) | High | |
| Base — Subsequent System | Confidentiality (SC) | None |
| Integrity (SI) | None | |
| Availability (SA) | None | |
| Threat | Exploit Maturity (E) | Proof-of-Concept |
| Field | Value |
|---|---|
| CVSS 4.0 Score | 8.2 (High) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P |
AT:Present because exploitation requires the target site to have a Forminator form containing both a File Upload field and a Select field. While this is a common form configuration, it is not guaranteed on every installation.
4. Affected Products
Affected Products
| Product | Version Range | CPE 2.3 |
|---|---|---|
| Forminator Forms (free) | <= 1.56.1 | cpe:2.3:a:wpmudev:forminator:*:*:*:*:*:wordpress:*:* |
| Forminator Pro | <= 1.56.1 | cpe:2.3:a:wpmudev:forminator:*:*:*:*:pro:wordpress:*:* |
Active installations: 600,000+
Tested Environment (Vulnerable)
| Field | Value |
|---|---|
| Platform | WordPress 6.5.5, PHP 8.2.21, Apache 2.4.59 |
| Plugin | Forminator Forms 1.56.1 |
| Architecture | x64 (Linux/Debian container) |
| Prerequisite | Form with File Upload + Select fields |
Tested Environment (Patched)
| Field | Value |
|---|---|
| Patch | Forminator Forms 1.56.2 (July 31, 2026) |
| Platform | WordPress 6.5.5, PHP 8.2.21, Apache 2.4.59 |
| Plugin | Forminator Forms 1.56.2 |
5. Root Cause Analysis
5a. Detailed Description
The vulnerability chains two independent weaknesses in the Forminator plugin’s form submission processing:
Weakness 1: Select field return key injection
When a custom form is submitted via AJAX (forminator_submit_form_custom-forms), the plugin parses POST data into $prepared_data through Forminator_Core::sanitize_array(). This sanitizer explicitly skips values for keys prefixed with select-, radio-, or checkbox-, returning them untouched to preserve structured option values:
// class-core.php, sanitize_array()
if (
false === $force && ( 0 === strpos( $current_key, 'url-' ) ||
0 === strpos( $current_key, 'select-' ) || // <-- SKIPPED
0 === strpos( $current_key, 'radio-' ) ||
0 === strpos( $current_key, 'checkbox-' ) || ... )
) {
return $data; // returned as-is, no sanitization
}
The unsanitized Select field data then reaches set_field_data() in Forminator_CForm_Front_Action. This method retrieves the submitted value from $prepared_data and checks for a return key:
// front-action.php, set_field_data()
$field_data = self::$prepared_data[ $field_id ]; // attacker-controlled array
$field_data = apply_filters( 'forminator_handle_specific_field_types', $field_data, ... );
if ( ! empty( $field_data['return'] ) ) {
unset( $field_data['return'] );
self::$info['field_data_array'][] = $field_data; // INJECTED DIRECTLY
return; // skips all validation
}
Because PHP’s wp_parse_str() parses select-1[return]=1 as a nested array, an attacker can inject arbitrary keys — including name, field_type, field_array, and value — into the internal field_data_array. This creates a forged upload field record that process_uploads() will later process as a legitimate file upload.
Weakness 2: Extension blocklist exact-key bypass
The forminator_allowed_mime_types() function filters dangerous extensions from the allowed MIME types list using exact string matching:
// helper-fields.php, forminator_allowed_mime_types()
$filters = array( 'htm|html', 'js', ..., 'php', 'php3', ... );
foreach ( array_keys( $mimes ) as $mime_key ) {
$key = strtolower( $mime_key );
if ( in_array( $key, $filters, true ) ) { // EXACT match only
unset( $mimes[ $mime_key ] );
}
}
The blocklist contains php as an exact entry. However, the attacker controls the MIME type map keys through the forged upload field configuration. Submitting a key like php|text/x-php bypasses the exact match (the blocklist has no entry for php|text/x-php), but WordPress’s wp_check_filetype() treats these keys as regex patterns:
// WordPress core, wp_check_filetype()
foreach ( $mimes as $ext_preg => $mime_match ) {
$ext_preg = '!\.(' . $ext_preg . ')$!i';
if ( preg_match( $ext_preg, $filename, $ext_matches ) ) { ... }
}
The pattern !\.(php|text/x-php)$!i matches .php files, so the upload validation succeeds.
Additionally, the attacker sets the additional-type field property to php|text/x-php, which adds ['php' => 'text/x-php'] to WordPress’s upload_mimes filter via check_mime_type(). This causes wp_check_filetype_and_ext() (the content-based MIME validator) to accept PHP files as well.
Complete exploit chain:
- Fetch the form page to obtain the nonce, form ID, and field IDs
- Submit a multipart POST with the Select field value as a nested array containing
return=1and a forged upload field configuration - Include a legitimate file for the real Upload field (triggers
$has_upload=trueviahandle_upload_field()incheck_fields_visibility()) - Include the malicious PHP file under the injected field name
process_uploads()iteratesfield_data_array, finds the forged upload record, and callshandle_file_upload()with the attacker-controlled field configuration- The PHP file passes both extension and content-based validation and is saved to the Forminator uploads directory
- Accessing the uploaded file executes arbitrary PHP code (on Nginx or when
.htaccessis absent)
5b. Vulnerable Code and Call Stack
front-action.php (Forminator 1.56.1):
set_field_data():
$field_data = self::$prepared_data[ $field_id ]; // attacker array from POST
$field_data = apply_filters( 'forminator_handle_specific_field_types', ... );
if ( ! empty( $field_data['return'] ) ) { // truthy '1'
unset( $field_data['return'] );
self::$info['field_data_array'][] = $field_data; // forged record injected
return; // validation bypassed
}
helper-fields.php (Forminator 1.56.1):
forminator_allowed_mime_types():
$filters = array( ..., 'php', ... );
foreach ( array_keys( $mimes ) as $mime_key ) {
$key = strtolower( $mime_key );
if ( in_array( $key, $filters, true ) ) { // 'php|text/x-php' != 'php'
unset( $mimes[ $mime_key ] ); // NOT reached — bypass
}
}
Call Stack:
Forminator_CForm_Front_Action::submit_form_custom_forms()
-> init_properties() // parses $_POST into prepared_data
-> check_fields_visibility() // calls handle_upload_field() -> sets $has_upload
-> handle_form()
-> prepare_fields_info()
-> set_field_data_array()
-> set_field_data() // return key bypass injects forged record
-> check_errors()
-> filter_field_data_array()
-> process_uploads('upload')
-> handle_file_upload() // processes forged record with attacker config
-> forminator_allowed_mime_types() // blocklist bypassed
-> wp_check_filetype() // regex matches .php
-> check_mime_type() // additional-type adds PHP to allowed list
-> move_uploaded_file() // PHP file saved to disk
5c. Fix (Patched Version)
Forminator 1.56.2 applies two fixes:
Fix 1 — Strip return from user data (front-action.php):
// Added before the forminator_handle_specific_field_types filter
if ( is_array( $field_data ) ) {
unset( $field_data['return'] );
}
This prevents client-supplied data from triggering the early-return injection path. Only the forminator_handle_specific_field_types filter (a server-side hook that attackers cannot control) can now set the return key.
Fix 2 — Normalize pipe-separated MIME keys (helper-fields.php):
| Vulnerable (1.56.1) | Patched (1.56.2) |
|---|---|
$filters = array('htm|html', 'php', ...) | $blocked_extensions = array('htm', 'html', 'php', ...) |
in_array($key, $filters, true) (exact match) | Split on |, preg_replace('/[^a-z0-9]/', '', ...), check each alternative |
php|text/x-php bypasses php | php|text/x-php → split → php matches blocklist |
The patched code also normalizes regex metacharacters: ph(p) becomes php after preg_replace('/[^a-z0-9]/', '', ...), catching pattern-based bypasses.
5d. Impact
The vulnerability allows an unauthenticated remote attacker to upload and execute arbitrary PHP code on WordPress sites running Forminator Forms <= 1.56.1. Successful exploitation achieves full remote code execution under the web server’s user context (typically www-data), enabling complete site takeover including database access, file system manipulation, lateral movement to other virtual hosts, and installation of persistent backdoors. The attack requires no authentication and no user interaction.
The default Forminator upload directory includes an .htaccess file that blocks PHP execution on Apache, which limits direct exploitation on Apache-based hosting. However, Nginx servers ignore .htaccess entirely, and custom upload storage configurations may lack this protection. Given the plugin’s 600,000+ active installations and the trivial exploitation requirements, the vulnerability poses a significant risk particularly to Nginx-hosted WordPress sites and sites with custom upload configurations.
6. Proof-of-Concept
6a. PoC Code
The PoC script is provided as poc_cve_2026_15748.py.
6b. Reproduce Instructions
Prerequisites:
- WordPress installation with Forminator Forms <= 1.56.1
- A published form containing both a File Upload field and a Select field
- Python 3 with
requestslibrary
Steps:
Identify the target WordPress site and a page containing a Forminator form with Upload and Select fields.
Run the PoC:
python3 poc_cve_2026_15748.py http://target.example.com /contact/The script will:
- Fetch the form page and extract the nonce, form ID, and field IDs
- Craft a multipart POST with the Select field injection payload
- Upload a PHP verification file via the forged upload record
- Attempt to locate and execute the uploaded file
On success, the script exits with code 0 and reports the uploaded file location.
To verify RCE, access the uploaded PHP file directly:
http://target.example.com/wp-content/uploads/forminator/<form_id>_<hash>/uploads/<random>-<filename>.php
6c. Test Results
| Metric | Vulnerable (1.56.1) | Patched (1.56.2) |
|---|---|---|
| Form submission | success: true | success: false |
| PHP file uploaded | Yes | No |
| PHP execution (no .htaccess) | Yes (RCE confirmed) | N/A |
| PoC exit code | 0 | 1 |
| Server error | None | “Selected value does not exist.” |
6d. Patched System Verification
Running the same PoC against Forminator 1.56.2 produces a validation error. The patched set_field_data() strips the return key from the Select field’s submitted array data before the injection check. The field data then falls through to the normal Select field validation path, which correctly rejects the nested array because it does not match any configured select option value. No files are uploaded, and the PoC exits with code 1.
7. Detection
Section 7A: Network-Based Detection
Signature-Based Detection
The exploit traffic is distinguishable from legitimate form submissions by the presence of nested array parameters in Select field POST data. Normal select field submissions send a scalar value (e.g., select-1=option_a), while the exploit sends array keys like select-1[return]=1, select-1[field_type]=upload, and select-1[field_array][...]. The multipart body also contains a PHP file upload alongside the form data.
Suricata Rules
alert http $EXTERNAL_NET any -> $HOME_NET any (
msg:"CVE-2026-15748 Forminator Select Field Injection - return key";
flow:established,to_server;
http.method; content:"POST";
http.uri; content:"/wp-admin/admin-ajax.php";
http.request_body; content:"forminator_submit_form";
http.request_body; content:"select-";
http.request_body; content:"[return]";
http.request_body; content:"[field_type]";
classtype:web-application-attack;
reference:cve,2026-15748;
sid:2026015748; rev:1;
)
alert http $EXTERNAL_NET any -> $HOME_NET any (
msg:"CVE-2026-15748 Forminator PHP Upload via MIME Key Bypass";
flow:established,to_server;
http.method; content:"POST";
http.uri; content:"/wp-admin/admin-ajax.php";
http.request_body; content:"forminator_submit_form";
http.request_body; content:".php";
http.request_body; content:"filetypes";
http.request_body; pcre:"/php\x7c/";
classtype:web-application-attack;
reference:cve,2026-15748;
sid:2026015749; rev:1;
)
alert http $EXTERNAL_NET any -> $HOME_NET any (
msg:"CVE-2026-15748 Webshell Access in Forminator Uploads";
flow:established,to_server;
http.method; content:"GET";
http.uri; content:"/wp-content/uploads/forminator/";
http.uri; content:".php";
classtype:web-application-attack;
reference:cve,2026-15748;
sid:2026015750; rev:1;
)
Section 7B: Host-Based Detection
Version Identification
# Check Forminator plugin version
grep -oP "Version:\s*\K[0-9.]+" /path/to/wp-content/plugins/forminator/forminator.php
Versions <= 1.56.1 are vulnerable. Update to 1.56.2 or later.
Service/Application Logs
The exploit produces POST requests to /wp-admin/admin-ajax.php with forminator_submit_form_custom-forms as the action. The key distinguishing feature in access logs is the POST body size (larger than typical form submissions due to the nested array injection) and the presence of multipart file uploads.
# Detect exploit attempts in Apache/Nginx access logs
grep -P 'POST.*admin-ajax\.php.*forminator' /var/log/apache2/access.log | \
grep -P 'select-\d+%5Breturn%5D|select-\d+\[return\]'
# Detect webshell access
grep -P 'GET.*uploads/forminator/.*\.php' /var/log/apache2/access.log
Forensic Artifacts
After successful exploitation, the following artifacts remain:
- Uploaded PHP files in
/wp-content/uploads/forminator/<form_id>_<hash>/uploads/with a random 12-character alphanumeric prefix (e.g.,aB3dEf4gH1jK-shell.php) - Forminator form entries in the
wp_frmt_form_entryandwp_frmt_form_entry_metadatabase tables recording the submission - Apache/Nginx access log entries showing POST to
admin-ajax.phpfollowed by GET to the uploaded PHP file
# Find PHP files in Forminator upload directories (excluding index.php)
find /path/to/wp-content/uploads/forminator/ -name "*.php" ! -name "index.php" -type f
# Check database for suspicious entries
wp db query "SELECT e.entry_id, e.date_created, m.meta_value
FROM wp_frmt_form_entry e
JOIN wp_frmt_form_entry_meta m ON e.entry_id = m.entry_id
WHERE m.meta_value LIKE '%.php%'
ORDER BY e.date_created DESC LIMIT 20;" --allow-root