On August 9th, 2026, Wordfence Argus, created by the Wordfence Threat Intelligence team, discovered an Arbitrary File Upload vulnerability in Gravity Forms, a WordPress plugin estimated to have more than one million active installations. This high-severity vulnerability makes it possible for unauthenticated threat actors to write files with attacker-selected extensions to a public temporary upload directory. This can lead to remote code execution.

We discovered this vulnerability with the help of Wordfence Argus, which we covered in a separate post. Our mission is to secure WordPress through defense in depth, which is why we invest in quality vulnerability research and work closely with plugin vendors to ensure vulnerabilities are addressed before they can be widely exploited. We are committed to making the WordPress ecosystem more secure through the detection and prevention of vulnerabilities, which is a critical element of a multi-layered approach to security.

We provided full disclosure details to the Gravity Forms team through our Wordfence Vulnerability Management Portal on August 11, 2026. The developer acknowledged the report on August 20, 2026, and released Gravity Forms 3.0.3 with the patch the same day. We would like to commend the Gravity Forms team for their response and remediation of this issue.

Wordfence Premium, Wordfence Care, and Wordfence Response customers received a firewall rule to provide protection against known exploitation techniques on August 13, 2026. Free users will receive the same rule 30 days later, on September 12, 2026.

We urge users to update their sites to Gravity Forms 3.0.3 or a newer version as soon as possible.

Vulnerability Summary from Wordfence Intelligence

CVSS Rating
8.1 (High)
Affected Version(s)
<= 3.0.2
Patched Version
3.0.3
Affected Software
Gravity Forms [gravityforms]
The Gravity Forms plugin for WordPress is vulnerable to Arbitrary File Upload in all versions up to, and including, 3.0.2. This is due to insufficient validation of multi-file upload chunk state in the `GFAsyncUpload::upload()` function, where public form state URL hashes can be reused as chunk continuation hashes and attacker-controlled temporary filenames are accepted before sanitization. This makes it possible for unauthenticated attackers, when a public form contains a File Upload field with Multiple Files enabled, to upload a valid PNG/PDF polyglot to an attacker-selected public `.php` or `.html` filename in the Gravity Forms temporary upload directory. This can lead to remote code execution on WordPress systems that use NGINX or other non `.htaccess` respecting web servers. NOTE: During installation and activation, the Gravity Forms plugin places a `.htaccess` file in this directory, which prevents this vulnerability from being exploited despite the PHP file being written to the temporary upload directory. In these cases where PHP execution is blocked, attacker-written HTML can result in stored same-origin cross-site scripting if a victim visits the generated file URL.

Technical Analysis

Gravity Forms includes a multi-file upload endpoint that supports chunked upload requests. A continuation request identifies the temporary file and supplies a state intended to associate it with a previous chunk number, form, field, and uploaded filename. Gravity Forms 3.0.2 attempted to authenticate those values with a WordPress hash.

Unfortunately, the implementation used the same wp_hash() operation for two unrelated security contexts. Public form state includes a hash of the current page URL in includes/form-display/state/class-state-handler.php:

private function add_url( $form_id ) {
	$url = $this->get_url();
	if ( str_contains( $url, '?' ) ) {
		$values = array(
			wp_hash( $url ),
			wp_hash( strtok( $url, '?' ) ),
		);
		$this->add_hashes( $form_id, 'url', $values );
	} else {
		$this->add_hashes( $form_id, 'url', wp_hash( $url ) );
	}
}

The chunk uploader used wp_hash() again, without a context-specific prefix, to authenticate a pipe-delimited set of continuation values in includes/upload.php:

private static function get_chunk_hash( $tmp_file_name, $chunk, $form_id, $field_id, $uploaded_filename ) {
	return wp_hash(
		implode(
			'|',
			array(
				$tmp_file_name,
				$chunk,
				$form_id,
				$field_id,
				$uploaded_filename,
			)
		)
	);
}

Because the public page URL is attacker-influenced, an unauthenticated attacker can request a URL whose value matches a chosen serialization of the chunk fields. The resulting URL hash from the public form state is then also a valid chunk-continuation hash. This is a cryptographic domain-confusion issue: the attacker does not need to recover the site’s secret or forge a new hash, but instead reuses a legitimate hash generated for a different purpose.

The impact is compounded by how GFAsyncUpload::upload() handles the supplied continuation state. In Gravity Forms 3.0.2, the handler verifies the hash over the raw, attacker-controlled temp_filename, assigns that value as the destination name, and only then passes it through sanitize_file_name():

$chunk         = isset( $_REQUEST['chunk'] ) ? intval( $_REQUEST['chunk'] ) : 0;
$chunks        = isset( $_REQUEST['chunks'] ) ? intval( $_REQUEST['chunks'] ) : 0;
$chunk_data    = $chunks && $file_name ? rgar( $_REQUEST, str_replace( '.', '_', $file_name ) ) : array();
$tmp_file_name = '';

if ( $chunk ) {
	if ( empty( $chunk_data['hash'] ) || ( $chunk_data['hash'] !== self::get_chunk_hash( $chunk_data['temp_filename'], ( $chunk - 1 ), $form_id, $field_id, $uploaded_filename ) ) ) {
		GFCommon::log_debug( __METHOD__ . sprintf( '(): Invalid hash for chunk #%d.', $chunk ) );
		self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
	}
	$tmp_file_name = $chunk_data['temp_filename'];
}

if ( empty( $tmp_file_name ) ) {
	$tmp_file_name = $form_unique_id . '_input_' . $field_id . '_' . GFCommon::random_str( 16 ) . '_' . $file_name;
}

$tmp_file_name = sanitize_file_name( $tmp_file_name );
$file_path     = $target_dir . $tmp_file_name;

The plugin validates the apparent upload names against the field’s allowed file types, but the temporary destination basename is handled separately. An attacker can therefore provide a normal, permitted carrier name such as safe.png while causing the sanitized temporary destination to end in .php. A valid PNG/PHP polyglot (a single file structured to be valid in two or more different file formats) satisfies the image content checks while retaining embedded PHP code that could be executed by the web server.

The continuation path also does not require a server-created first chunk. For a nonzero chunk, the handler opens the selected .part path in append mode, which creates the file if it does not already exist:

$out = @fopen( "{$file_path}.part", $chunk == 0 ? 'wb' : 'ab' );

If the request identifies itself as the final chunk, the plugin immediately removes the .part suffix:

if ( ! $chunks || $chunk == $chunks - 1 ) {
	// Upload is complete. Strip the temp .part suffix off
	rename( "{$file_path}.part", $file_path );

An attacker can exploit this issue when a public form contains a File Upload field with the Multiple Files option enabled. Gravity Forms and WordPress permit PNG and PDF uploads by default, so exploitation does not require an administrator to add either type to a custom allowlist. This allows for an unauthenticated public file write with an attacker-selected extension in the Gravity Forms temporary upload directory.

The final impact depends on the server configuration. Gravity Forms creates an upload-root .htaccess file with directives that disable PHP parsing on typical Apache configurations. NGINX does not process .htaccess. On servers that execute PHP in the directory, the write can lead to remote code execution. On servers where an effective .htaccess prevents PHP execution, an attacker can alternatively write a .html file, which can result in same-origin cross-site scripting if a victim visits the generated URL.

The Patch

The Gravity Forms team addressed this issue in version 3.0.3 by making temporary upload names server-generated and by requiring continuation chunks to present authenticated server-created state. The upload handler now generates a random temporary basename using only the permitted extension, validates the signed state before continuing an upload, and verifies that the expected partial file exists at the exact signed byte offset:

if ( $chunks && $chunk ) {
	$submitted_tmp_file_name = rgar( $chunk_data, 'temp_filename' );
	$chunk_state             = self::decode_chunk_token( rgar( $chunk_data, 'hash' ) );

	if ( ! self::is_valid_chunk_state( $chunk_state, $submitted_tmp_file_name, $chunk, $form_id, $field_id, $chunks, $uploaded_filename ) ) {
		GFCommon::log_debug( __METHOD__ . sprintf( '(): Invalid hash for chunk #%d.', $chunk ) );
		self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
	}

	$tmp_file_name = $chunk_state['temp_filename'];
	$write_offset  = $chunk_state['offset'];
}

if ( empty( $tmp_file_name ) ) {
	$tmp_file_name = 'gf_' . GFCommon::random_str( 32 ) . '.' . pathinfo( $file_name, PATHINFO_EXTENSION );
}

$tmp_file_name = sanitize_file_name( $tmp_file_name );
if ( ! self::is_valid_temp_filename( $tmp_file_name ) ) {
	self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
}

$file_path = $target_dir . $tmp_file_name;
if ( $chunks && $chunk && ( ! file_exists( "{$file_path}.part" ) || filesize( "{$file_path}.part" ) !== $write_offset ) ) {
	self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
}

Version 3.0.3 also replaces the ambiguous wp_hash() value with a structured, domain-separated HMAC token. The token binds the server-generated temporary filename to the next chunk number, form and field IDs, original filename, current byte offset, and total chunk count:

private static function get_chunk_hash( $tmp_file_name, $chunk, $form_id, $field_id, $uploaded_filename, $offset, $chunks ) {
	$payload = wp_json_encode(
		array(
			'temp_filename'     => (string) $tmp_file_name,
			'next_chunk'        => (int) $chunk,
			'form_id'           => (int) $form_id,
			'field_id'          => (int) $field_id,
			'uploaded_filename' => (string) $uploaded_filename,
			'offset'            => (int) $offset,
			'total_chunks'      => (int) $chunks,
		)
	);

	$encoded_payload = rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' );

	return $encoded_payload . '.' . hash_hmac( 'sha256', 'gravityforms-upload-chunk-v1|' . $encoded_payload, wp_salt( 'auth' ) );
}

Finally, the patched code rejects non-canonical temporary basenames and any temporary filename with a disallowed extension:

private static function is_valid_temp_filename( $tmp_file_name ) {
	if ( ! is_string( $tmp_file_name ) || $tmp_file_name === '' ) {
		return false;
	}

	if ( sanitize_file_name( $tmp_file_name ) !== $tmp_file_name ) {
		return false;
	}

	if ( wp_basename( $tmp_file_name ) !== $tmp_file_name ) {
		return false;
	}

	if ( GFCommon::file_name_has_disallowed_extension( $tmp_file_name ) ) {
		return false;
	}

	return true;
}

These changes prevent a public form-state hash from authenticating upload continuation state, remove attacker control over the destination basename, and ensure that a continuation request belongs to an upload session actually created by the server. Gravity Forms lists version 3.0.3 as released on August 20, 2026, with security enhancements in its official changelog.

Wordfence Firewall

The following graphic demonstrates the steps to exploitation an attacker might take and at which point the Wordfence firewall would block an attacker from successfully exploiting the vulnerability.

Diagram showing the Wordfence Firewall blocking an unauthenticated malicious Gravity Forms file upload request

Disclosure Timeline

2026-08-09

We discovered the vulnerability
During internal research with Wordfence Argus, we discovered an unauthenticated Arbitrary File Upload vulnerability in Gravity Forms.
2026-08-11

We validated the report and disclosed it to the vendor
Our team confirmed the proof of concept and sent full disclosure details to the Gravity Forms team through the Wordfence Vulnerability Management Portal.
2026-08-13

Wordfence Premium, Care, and Response users received a firewall rule
We released a firewall rule protecting Wordfence Premium, Care, and Response customers against known exploitation techniques.
2026-08-20

Vendor acknowledged the report
The Gravity Forms team acknowledged the report.
2026-08-20

Patched version released
Gravity Forms 3.0.3, the first version containing the complete patch, was released.
2026-09-12

Wordfence free users receive the firewall rule
Sites running the free version of Wordfence receive the same firewall rule 30 days after the Premium release.
Wordfence action
Vendor / external action

Conclusion

In this blog post, we detailed an Arbitrary File Upload vulnerability in the Gravity Forms plugin affecting versions 3.0.2 and earlier. On sites with a public form containing a multi-file upload field, unauthenticated threat actors can write a valid image/PHP polyglot to a public temporary upload path with an attacker-selected .php extension. Where the server executes PHP in that directory, this can result in remote code execution.

The vulnerability has been addressed in Gravity Forms 3.0.3. We encourage WordPress users to verify that their sites are running version 3.0.3 or newer as soon as possible due to the high severity of this vulnerability.

Wordfence Premium, Wordfence Care, and Wordfence Response users received a firewall rule to protect against exploits targeting this vulnerability on August 13, 2026. Sites using the free version of Wordfence will receive the same protection on September 12, 2026.

If you know someone who uses Gravity Forms on their site, we recommend sharing this advisory with them to help ensure their site remains secure.

The post Wordfence Argus Finds Unauthenticated Arbitrary File Upload Vulnerability in Gravity Forms appeared first on Wordfence.