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

FieldValue
Primary CWECWE-434: Unrestricted Upload of File with Dangerous Type
Related CWECWE-20: Improper Input Validation

3. Severity

CVSS 3.1 (from Wordfence Advisory)

FieldValue
Score9.8 (Critical)
VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Our Assessment (CVSS 4.0)

Metric GroupMetricValue
Base — ExploitabilityAttack Vector (AV)Network
Attack Complexity (AC)Low
Attack Requirements (AT)Present
Privileges Required (PR)None
User Interaction (UI)None
Base — Vulnerable SystemConfidentiality (VC)High
Integrity (VI)High
Availability (VA)High
Base — Subsequent SystemConfidentiality (SC)None
Integrity (SI)None
Availability (SA)None
ThreatExploit Maturity (E)Proof-of-Concept
FieldValue
CVSS 4.0 Score8.2 (High)
CVSS 4.0 VectorCVSS: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

ProductVersion RangeCPE 2.3
Forminator Forms (free)<= 1.56.1cpe:2.3:a:wpmudev:forminator:*:*:*:*:*:wordpress:*:*
Forminator Pro<= 1.56.1cpe:2.3:a:wpmudev:forminator:*:*:*:*:pro:wordpress:*:*

Active installations: 600,000+

Tested Environment (Vulnerable)

FieldValue
PlatformWordPress 6.5.5, PHP 8.2.21, Apache 2.4.59
PluginForminator Forms 1.56.1
Architecturex64 (Linux/Debian container)
PrerequisiteForm with File Upload + Select fields

Tested Environment (Patched)

FieldValue
PatchForminator Forms 1.56.2 (July 31, 2026)
PlatformWordPress 6.5.5, PHP 8.2.21, Apache 2.4.59
PluginForminator 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:

  1. Fetch the form page to obtain the nonce, form ID, and field IDs
  2. Submit a multipart POST with the Select field value as a nested array containing return=1 and a forged upload field configuration
  3. Include a legitimate file for the real Upload field (triggers $has_upload=true via handle_upload_field() in check_fields_visibility())
  4. Include the malicious PHP file under the injected field name
  5. process_uploads() iterates field_data_array, finds the forged upload record, and calls handle_file_upload() with the attacker-controlled field configuration
  6. The PHP file passes both extension and content-based validation and is saved to the Forminator uploads directory
  7. Accessing the uploaded file executes arbitrary PHP code (on Nginx or when .htaccess is 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 phpphp|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 requests library

Steps:

  1. Identify the target WordPress site and a page containing a Forminator form with Upload and Select fields.

  2. Run the PoC:

    python3 poc_cve_2026_15748.py http://target.example.com /contact/
    
  3. 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
  4. On success, the script exits with code 0 and reports the uploaded file location.

  5. 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

MetricVulnerable (1.56.1)Patched (1.56.2)
Form submissionsuccess: truesuccess: false
PHP file uploadedYesNo
PHP execution (no .htaccess)Yes (RCE confirmed)N/A
PoC exit code01
Server errorNone“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_entry and wp_frmt_form_entry_meta database tables recording the submission
  • Apache/Nginx access log entries showing POST to admin-ajax.php followed 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

8. References

SourceURL
NVDhttps://nvd.nist.gov/vuln/detail/CVE-2026-15748
Wordfence Advisoryhttps://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/forminator
SecurityOnlinehttps://securityonline.info/cve-2026-15748-forminator-rce/
The Hacker Newshttps://thehackernews.com/2026/08/forminator-wordpress-flaw-can-enable.html
WordPress Pluginhttps://wordpress.org/plugins/forminator/
Vendor Patchhttps://downloads.wordpress.org/plugin/forminator.1.56.2.zip