CVE-2025-14854

WP-CRM System – Manage Clients and Projects <= 3.4.5 - Missing Authorization to Authenticated (Subscriber+) CRM Data Exposure and Task Modification

mediumMissing Authorization
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
3.4.6
Patched in
21d
Time to patch

Description

The WP-CRM System plugin for WordPress is vulnerable to unauthorized access due to missing capability checks on the wpcrm_get_email_recipients and wpcrm_system_ajax_task_change_status AJAX functions in all versions up to, and including, 3.4.5. This makes it possible for authenticated attackers, with subscriber level access and above, to enumerate CRM contact email addresses (PII disclosure) and modify CRM task statuses. CVE-2025-62106 is likely a duplicate of this issue.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.4.5
PublishedJanuary 13, 2026
Last updatedFebruary 3, 2026
Affected pluginwp-crm-system

What Changed in the Fix

Changes introduced in v3.4.6

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: WP-CRM System <= 3.4.5 Missing Authorization ## 1. Vulnerability Summary The **WP-CRM System** plugin for WordPress (versions <= 3.4.5) contains a missing authorization vulnerability within its AJAX handlers. Specifically, functions responsible for retrieving CRM task …

Show full research plan

Exploitation Research Plan: WP-CRM System <= 3.4.5 Missing Authorization

1. Vulnerability Summary

The WP-CRM System plugin for WordPress (versions <= 3.4.5) contains a missing authorization vulnerability within its AJAX handlers. Specifically, functions responsible for retrieving CRM task details (wp_crm_system_ajax_task_list) and updating task statuses (wpcrm_system_ajax_task_change_status) are registered via wp_ajax_ but fail to implement capability checks (e.g., current_user_can()).

This allows any authenticated user, including those with Subscriber level permissions, to:

  1. Enumerate CRM Data: View details of any task by ID, including associated contact names, organization names, and project names.
  2. Modify Task Statuses: Change the status of any CRM task.
  3. PII Disclosure (Inferred): The description also mentions wpcrm_get_email_recipients (code not provided) allowing enumeration of contact email addresses.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions:
    • task_change_status (Triggers wpcrm_system_ajax_task_change_status)
    • task_list_response (Triggers wp_crm_system_ajax_task_list)
    • wpcrm_get_email_recipients (Triggers inferred PII disclosure)
  • Authentication: Authenticated, Subscriber level or higher (wp_ajax_ hooks).
  • Preconditions: A valid CRM task ID must exist in the database for the exploit to show impact.

3. Code Flow

Task Modification (task_change_status)

  1. Entry Point: add_action( 'wp_ajax_task_change_status', 'wpcrm_system_ajax_task_change_status' ); in includes/wcs-dashboard-task-list.php.
  2. Input: Takes post_id and task_status from $_POST.
  3. Processing: wpcrm_system_ajax_task_change_status() directly calls update_post_meta( $post_id, '_wpcrm_task-status', $task_status ).
  4. Vulnerability: There is no nonce verification and no capability check. Any logged-in user can invoke this.

CRM Data Disclosure (task_list_response)

  1. Entry Point: add_action( 'wp_ajax_task_list_response', 'wp_crm_system_ajax_task_list' ); in includes/wcs-dashboard-task-list.php.
  2. Input: Takes task_id and task_list_nonce from $_POST.
  3. Processing: wp_crm_system_ajax_task_list() verifies the nonce, then retrieves task metadata:
    • _wpcrm_task-attach-to-organization
    • _wpcrm_task-attach-to-contact
    • _wpcrm_task-attach-to-project
  4. Output: Returns an HTML table containing titles of the organization, contact, and project.
  5. Vulnerability: While a nonce check exists, there is no capability check. Any user who can obtain the task-list-nonce (localized on admin pages) can view any task's internal CRM associations.

4. Nonce Acquisition Strategy

The task_change_status action does not require a nonce.
The task_list_response action requires task_list_nonce.

Strategy to obtain task_list_nonce:

  1. Localization: The nonce is localized in wpcrm_system_dashboard_task_js via wp_localize_script.
  2. Target Hook: It is hooked to admin_enqueue_scripts, meaning it is likely present on any admin page for an authenticated user.
  3. Extraction:
    • Login as a Subscriber.
    • Navigate to /wp-admin/profile.php.
    • Use browser_eval to extract: window.task_list_vars?.task_list_nonce.

5. Exploitation Strategy

The agent will perform two primary exploits: unauthorized task status modification and unauthorized data disclosure.

Step 1: Unauthorized Task Status Modification

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=task_change_status
    &post_id=[TASK_ID]
    &task_status=completed
    
  • Expected Response: success

Step 2: Unauthorized Data Disclosure

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=task_list_response
    &task_list_nonce=[EXTRACTED_NONCE]
    &task_id=[TASK_ID]
    
  • Expected Response: An HTML table containing titles of CRM objects (Contacts/Projects) that the Subscriber should not be able to see.

6. Test Data Setup

To demonstrate impact, the environment must contain CRM data:

  1. Create a CRM Contact:
    wp post create --post_type=wpcrm-contact --post_title="Secret Client" --post_status=publish
  2. Create a CRM Project:
    wp post create --post_type=wpcrm-project --post_title="Confidential Project" --post_status=publish
  3. Create a CRM Task and link it:
    • Create task: TASK_ID=$(wp post create --post_type=wpcrm-task --post_title="Initial Task" --post_status=publish --porcelain)
    • Attach contact: wp post meta add $TASK_ID _wpcrm_task-attach-to-contact [CONTACT_ID]
    • Set initial status: wp post meta add $TASK_ID _wpcrm_task-status "not-started"
  4. Create Subscriber User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password

7. Expected Results

  1. Modification: The task_change_status request returns "success". Verification via WP-CLI shows the post meta _wpcrm_task-status changed from not-started to completed.
  2. Disclosure: The task_list_response returns HTML containing the string "Secret Client" and "Confidential Project", proving the Subscriber accessed restricted CRM relationships.

8. Verification Steps

After performing the HTTP requests:

  1. Check Status Change:
    wp post meta get [TASK_ID] _wpcrm_task-status
    (Expected: completed)
  2. Verify Access to Contact Name: Compare the HTTP response from task_list_response against the string "Secret Client".

9. Alternative Approaches

If task_list_nonce is not present on profile.php, the agent should check if the plugin registers a specific dashboard page for tasks.
Search for add_menu_page or add_submenu_page in the code:
grep -r "add_submenu_page" includes/
If a "Tasks" page exists that a Subscriber can access, the nonce will certainly be there.

If wpcrm_get_email_recipients is the target:

  1. Identify the action via grep -r "wp_ajax_wpcrm_get_email_recipients" .
  2. Send a POST request to admin-ajax.php with action=wpcrm_get_email_recipients.
  3. Observe if it returns a list of email addresses from the wpcrm-contact posts.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP-CRM System plugin for WordPress contains missing authorization and nonce checks in its AJAX and GDPR-related handlers. This allows authenticated users with subscriber-level permissions to modify CRM task statuses, enumerate CRM task details (including linked organizations and contacts), and potentially access or delete PII through improperly secured GDPR export/deletion features.

Vulnerable Code

// includes/wcs-dashboard-task-list.php (approx. line 173)
function wpcrm_system_ajax_task_change_status() {
	$post_id     = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : '';
	$task_status = isset( $_POST['task_status'] ) ? sanitize_text_field( $_POST['task_status'] ) : '';
	$meta        = 'status';

	$update_status = update_post_meta( $post_id, '_wpcrm_task-' . $meta, $task_status );

	if ( $update_status ) {
		echo 'success';
	} else {
		echo 'fail';
	}
	exit;
}

---

// includes/wcs-dashboard-task-list.php (approx. line 7)
function wp_crm_system_ajax_task_list() {
	if ( ! isset( $_POST['task_list_nonce'] ) || ! wp_verify_nonce( $_POST['task_list_nonce'], 'task-list-nonce' ) ) {
		die( 'Permissions check failed' );
	}

	if ( ! isset( $_POST['task_id'] ) ) {
		die( 'No task data sent' );
	}
    // ... (retrieves and displays CRM task meta like organization, contact, and project names)

---

// includes/gdpr-export-contact.php (approx. line 28)
	public function get_cpt_post_ids(){
		$ids = array( $_GET['contact_id'] );

		return $ids;
	}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-crm-system/3.4.5/includes/gdpr-export-contact.php /home/deploy/wp-safety.org/data/plugin-versions/wp-crm-system/3.4.6/includes/gdpr-export-contact.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-crm-system/3.4.5/includes/gdpr-export-contact.php	2025-10-08 12:27:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-crm-system/3.4.6/includes/gdpr-export-contact.php	2026-01-19 10:41:28.000000000 +0000
@@ -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;
 	}

Exploit Outline

1. Authentication: Log in to the WordPress site as a user with Subscriber-level permissions. 2. Nonce Retrieval (for Data Disclosure): Access any admin page where the plugin enqueues its dashboard scripts (e.g., `/wp-admin/profile.php`). Extract the `task_list_nonce` from the `task_list_vars` JavaScript variable in the page source. 3. Task Modification: Send a POST request to `/wp-admin/admin-ajax.php` with the action `task_change_status`, a valid CRM task `post_id`, and a new `task_status`. The server will update the task status without a capability check or nonce verification. 4. Data Enumeration: Send a POST request to `/wp-admin/admin-ajax.php` with the action `task_list_response`, the retrieved `task_list_nonce`, and a valid `task_id`. The server returns an HTML table disclosing the titles and edit links for linked Organizations, Contacts, and Projects. 5. Unauthorized GDPR Access: If contact IDs are known, an attacker can attempt to access the GDPR export functionality by navigating to the GDPR page shortcode URL without a valid 'secret' parameter, as earlier versions lacked server-side verification of the secret token against stored post meta.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.