(647) 243-4688

On July 14th, 2026, we received a submission for an Unauthenticated Arbitrary File Upload vulnerability in Forminator Forms, a WordPress plugin with more than 600,000 active installations. This vulnerability makes it possible for unauthenticated attackers to upload arbitrary files, including executable PHP files, to a vulnerable site, which can lead to remote code execution and complete site compromise. The vulnerability is only exploitable on sites that have a form containing both a File Upload field and a Select field.

Props to daroo who discovered and responsibly reported this vulnerability through the Wordfence Bug Bounty Program. This researcher earned a bounty of $2,048.00 for this discovery. Our mission is to secure WordPress through defense in depth, which is why we are investing in quality vulnerability research and collaborating with researchers of this caliber through our Bug Bounty Program. We are committed to making the WordPress ecosystem more secure through the detection and prevention of vulnerabilities, which is a critical element to the multi-layered approach to security.

All Wordfence users, including those running Wordfence Premium, Wordfence Care, and Wordfence Response, as well as sites running the free version of Wordfence, are protected against any exploits targeting this vulnerability by the Wordfence firewall’s built-in Malicious File Upload protection.

We provided full disclosure details to the Forminator team through our Wordfence Vulnerability Management Portal on July 14, 2026. The developer acknowledged the report on July 20, 2026, and released the fully patched version on July 31, 2026. We would like to commend the Forminator team for their prompt response and timely patch.

We urge users to update their sites to the latest patched version of Forminator Forms, version 1.56.2 at the time of this publication, as soon as possible.

Vulnerability Summary from Wordfence Intelligence

CVSS Rating
9.8 (Critical)
Affected Version(s)
<= 1.56.1
Patched Version
1.56.2
Bounty
$2,048.00

The Forminator Forms plugin for WordPress is vulnerable to Arbitrary File Upload in all versions up to, and including, 1.56.1 via the handle_file_upload function. This is due to insufficient file type validation in handle_file_upload, where the dangerous-extension blocklist performs exact-key matching that is bypassed by pipe-alternative MIME type keys, combined with a public submission handler that trusts attacker-controlled upload field configuration injected via a forged Select field value. This makes it possible for unauthenticated attackers to upload files that may be executable, which makes remote code execution possible.

Technical Analysis

Forminator Forms is a popular drag-and-drop form builder plugin for WordPress that supports contact forms, polls, quizzes, and file uploads.

This vulnerability is the result of several issues that, when combined, allow an unauthenticated attacker to upload an executable PHP file to the web server.

Examining the code reveals that the plugin processes each submitted field through the set_field_data() function in the Forminator_CForm_Front_Action class. When a submitted field contains a return value, the handler appends every other attacker-supplied property to the internal field_data_array before the field’s own sanitizer and validator run:

private static function set_field_data( $field_id, $field_array, $field_index, $original_id = null, $stored_fields = array() ) {
    $field_type     = $field_array['type'];
    $form_field_obj = Forminator_Core::get_field_object( $field_type );
    // Skip if field object is not found.
    if ( empty( $form_field_obj ) ) {
        return;
    }
    if ( isset( self::$prepared_data[ $field_id ] ) ) {
        $field_data = self::$prepared_data[ $field_id ];
    } else {
        $field_data = array();
    }

    /**
     * Filter handle specific field types
     *
     * @since 1.13
     *
     * @param array  $field_data Field data
     * @param object $form_field_obj Form field object
     * @param array  $field_array field settings
     *
     * @return array $field_data Set `return` element of the array as true for returning
     */
    $field_data = apply_filters( 'forminator_handle_specific_field_types', $field_data, $form_field_obj, $field_array );

    if ( ! empty( $field_data['return'] ) ) {
        unset( $field_data['return'] );

        self::$info['field_data_array'][] = $field_data;
        return;
    }

This path is reachable because Forminator’s generic request sanitizer intentionally returns the complete nested value of select-*, radio-*, and checkbox-* fields unchanged, leaving field-specific handling for later. As a result, an unauthenticated attacker can use a Select field on the form as a carrier for a forged record, declaring it to be an Upload record and supplying its own name, field_type, and field_array properties. The Select field has no functional relationship to the File Upload field, it is simply the field type that reaches this nested-array return path.

Once a genuine File Upload field is present on the form, the upload-processing phase runs and iterates over every entry in field_data_array, including the record forged through the Select field. The process_uploads() function in the Forminator_Upload class trusts the field_type value, uses the supplied name to select a file input, and passes the attacker-supplied field_array to the upload handler as trusted field configuration:

private static function process_uploads( $mode ) {
    if ( self::$is_draft || ! self::$has_upload ) {
        return;
    }

    $fields = self::$info['field_data_array'];
    foreach ( $fields as $key => $field ) {
        if (
            ! isset( $field['field_type'] ) ||
            'upload' !== $field['field_type']
        ) {
            continue;
        }

        $field_id       = $field['name'];
        $field_settings = $field['field_array'];
        $file_type      = Forminator_Field::get_property( 'file-type', $field_settings, 'single' );
        $upload_method  = Forminator_Field::get_property( 'upload-method', $field_settings, 'ajax' );
        $form_field_obj = Forminator_Core::get_field_object( 'upload' );

        if ( 'upload' === $mode ) {

            if ( 'multiple' === $file_type && 'ajax' === $upload_method ) {
                continue;
            } elseif ( 'multiple' === $file_type && 'submission' === $upload_method ) {
                $form_upload_data = isset( $_FILES[ $field_id ] ) ? $_FILES[ $field_id ] : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput
                $upload_data      = $form_field_obj->handle_submission_multifile_upload( self::$module_id, $field_settings, $form_upload_data, self::$has_payment );
            } elseif ( 'single' === $file_type ) {
                $upload_data = $form_field_obj->handle_file_upload(
                    self::$module_id,
                    $field_settings,
                    array(),
                    self::$has_payment ? 'upload' : 'submit'
                );
            }

Because the attacker now controls the field configuration passed to the handle_file_upload() function in the Forminator_Upload class, they can enable custom file types and supply their own list of allowed extensions and MIME types via the additional-type property.

public function handle_file_upload( $form_id, $field, $post_data = array(), $upload_type = 'submit', $file_input = array() ) {
	$this->field           = $field;
	$id                    = self::get_property( 'element_id', $field );
	$field_name            = $id;
	$custom_limit_size     = true;
	$upload_limit          = self::get_property( 'upload-limit', $field );
	$filesize              = self::get_property( 'filesize', $field, 'MB' );
	$custom_file_type      = self::get_property( 'custom-files', $field, false );
	$use_library           = self::get_property( 'use_library', $field, false );
	$file_type             = self::get_property( 'file-type', $field, 'single' );
	$use_library           = filter_var( $use_library, FILTER_VALIDATE_BOOLEAN );
	$mime_types            = array();
	$additional_mime_types = array();

	if ( empty( $upload_limit ) ) {
		$custom_limit_size = false;
	}

	$custom_file_type = filter_var( $custom_file_type, FILTER_VALIDATE_BOOLEAN );
	if ( $custom_file_type ) {
		// check custom mime.
		$filetypes             = self::get_property( 'filetypes', $field, array(), 'array' );
		$additional            = str_replace( '.', '', self::get_property( 'additional-type', $field, '', 'string' ) );
		$additional_filetype   = array_map( 'trim', explode( ',', $additional ) );
		$additional_filetypes  = $this->get_additional_file_types( $additional_filetype );
		$additional_mime_types = $this->get_additional_file_mime_types( $additional_filetype );
		$all_file_type         = array_merge( $filetypes, $additional_filetypes );
		foreach ( $all_file_type as $filetype ) {
			// Mime type format = Key is the file extension with value as the mime type.
			$mime_types[ $filetype ] = $filetype;
		}
	}

	$file_object = array();
	if ( ! empty( $file_input ) ) {
		$file_object = $file_input;
	} elseif ( isset( $_FILES[ $field_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$file_object = $_FILES[ $field_name ]; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput
	}
	if ( ! empty( $file_object ) ) {
		if ( isset( $file_object['name'] ) && ! empty( $file_object['name'] ) ) {
			$file_name  = wp_generate_password( 12, false, false ) . '-' . sanitize_file_name( $file_object['name'] );
			$mime_types = forminator_allowed_mime_types( $mime_types, false );
			/**
			 * Filter mime types to be used as validation
			 *
			 * @since 1.6
			 *
			 * @param array $mime_types return null/empty array to use default WP file types @see https://codex.wordpress.org/Plugin_API/Filter_Reference/upload_mimes.
			 * @param array $field
			 */
			$mime_types     = apply_filters( 'forminator_upload_field_mime_types', $mime_types, $field );
			$valid          = wp_check_filetype( $file_name, $mime_types );

The plugin is expected to strip dangerous extensions such as php from the allowed list, which is handled by the forminator_allowed_mime_types() function:

function forminator_allowed_mime_types( $mimes = array(), $allow = true ) {
	if ( empty( $mimes ) ) {
		$mimes = get_allowed_mime_types();
	}
	if ( ! $allow ) {
		$filters = array( 'htm|html', 'js', 'jse', 'jar', 'php', 'php3', 'php4', 'php5', 'phtml', 'svg', 'swf', 'exe', 'html', 'htm', 'shtml', 'xhtml', 'xml', 'css', 'asp', 'aspx', 'jsp', 'sql', 'hta', 'dll', 'bat', 'com', 'sh', 'bash', 'py', 'pl', 'dfxp', 'rar' );
		foreach ( array_keys( $mimes ) as $mime_key ) {
			$key = strtolower( $mime_key );
			if ( in_array( $key, $filters, true ) ) {
				unset( $mimes[ $mime_key ] );
			}
		}
	}

	return $mimes;
}

Unfortunately, this blocklist performs exact-key matching. It removes the literal php key, but not the equivalent regex-compatible key ph(p). By supplying the extension and MIME pattern ph(p)|text/x-php, an attacker can slip a PHP file past the dangerous-extension check, since WordPress’s extension matcher still interprets ph(p) as a pattern matching the .php suffix while the supplied MIME mapping lets the file type validation succeed.

By chaining these issues together, an unauthenticated attacker can submit a published form that contains both a File Upload field and a Select field, use the Select field to inject a forged Upload record with the ph(p)|text/x-php pattern, and upload a PHP file to the server.

It is worth noting that, in a default configuration, files are uploaded into a directory protected by an .htaccess file that prevents PHP execution. However, if an administrator has configured a Custom File Upload Storage root, that root can end up without the .htaccess protection because it is created only when it is first needed, during a frontend request where the WordPress helper responsible for writing the .htaccess file is not loaded. In such a configuration, requesting the uploaded file directly causes the web server to execute the attacker-controlled PHP code.

As with all arbitrary file upload vulnerabilities, this can lead to complete site compromise through the use of webshells and other techniques.

Disclosure Timeline

July 11, 2026 – We received the submission for the Unauthenticated Arbitrary File Upload vulnerability in Forminator Forms via the Wordfence Bug Bounty Program.
July 14, 2026 – We validated the report and confirmed the proof-of-concept exploit.
July 14, 2026 – Full disclosure details were sent instantly to the vendor through our Wordfence Vulnerability Management Portal.
July 20, 2026 – The vendor submitted a patch for review.
July 31, 2026 – The fully patched version of the plugin, 1.56.2, was released.

Conclusion

In this blog post, we detailed an Unauthenticated Arbitrary File Upload vulnerability within the Forminator Forms plugin affecting all versions up to, and including, 1.56.1. This vulnerability allows unauthenticated threat actors to upload executable PHP files to a vulnerable site and achieve remote code execution. The vulnerability has been fully addressed in version 1.56.2 of the plugin.

We encourage WordPress users to verify that their sites are updated to the latest patched version of Forminator Forms as soon as possible considering the critical nature of this vulnerability.

All Wordfence users, including those running Wordfence Premium, Wordfence Care, and Wordfence Response, as well as sites running the free version of Wordfence, are fully protected against this vulnerability by the Wordfence firewall’s built-in Malicious File Upload protection.

If you know someone who uses this plugin on their site, we recommend sharing this advisory with them to ensure their site remains secure, as this vulnerability poses a significant risk.

The post 600,000 WordPress Sites Affected by Arbitrary File Upload Vulnerability in Forminator Forms WordPress Plugin appeared first on Wordfence.