CVE-2026-31917

ERP <= 1.16.10 - Authenticated (Crm agent+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
1.16.11
Patched in
70d
Time to patch

Description

The ERP plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 1.16.10 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with crm agent-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.16.10
PublishedFebruary 5, 2026
Last updatedApril 15, 2026
Affected pluginerp

What Changed in the Fix

Changes introduced in v1.16.11

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets a SQL Injection vulnerability in the **ERP** plugin (Complete HR, Accounting & CRM Suite) affecting version **1.16.10** and below. ### 1. Vulnerability Summary The vulnerability exists in the `erp_get_peoples()` function within `includes/functions-people.php`. This functi…

Show full research plan

This research plan targets a SQL Injection vulnerability in the ERP plugin (Complete HR, Accounting & CRM Suite) affecting version 1.16.10 and below.

1. Vulnerability Summary

The vulnerability exists in the erp_get_peoples() function within includes/functions-people.php. This function is the core utility for retrieving "peoples" (contacts, customers, employees, vendors) across all ERP modules. The function fails to use $wpdb->prepare() or adequate escaping when processing the meta_query argument, allowing an attacker to inject arbitrary SQL into the WHERE clause.

2. Attack Vector Analysis

  • Vulnerable Endpoint: The WordPress REST API. Specifically, endpoints that call erp_get_peoples(). The most likely target is the CRM contacts listing: GET /wp-json/erp/v1/crm/contacts.
  • Vulnerable Parameter: meta_query. Specifically the sub-parameters meta_key or meta_value.
  • Authentication Level: Authenticated users with CRM Agent permissions (crm_agent role) or higher.
  • Vulnerability Type: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
  • Preconditions: The CRM module must be active (which is the default for this plugin).

3. Code Flow

  1. Entry Point: A REST API request is made to /wp-json/erp/v1/crm/contacts.
  2. Controller: The request is handled by a controller (likely WeDevs\ERP\CRM\API\ContactsController) which extracts query parameters and passes them to a data-fetching function.
  3. Data Fetcher: The controller calls erp_crm_get_contacts() or directly calls erp_get_peoples() in includes/functions-people.php.
  4. Vulnerable Sink: erp_get_peoples() (Line 96 in includes/functions-people.php):
    if ( $meta_query ) {
        $sql['join'][] = "LEFT JOIN $pepmeta_tb as people_meta on people.id = people_meta.`erp_people_id`";
    
        $meta_key   = isset( $meta_query['meta_key'] ) ? $meta_query['meta_key'] : '';
        $meta_value = isset( $meta_query['meta_value'] ) ? $meta_query['meta_value'] : '';
        $compare    = isset( $meta_query['compare'] ) ? $meta_query['compare'] : '=';
    
        // INJECTION POINT: $meta_key and $meta_value are concatenated directly
        $sql['where'][] = "AND people_meta.meta_key='$meta_key' and people_meta.meta_value='$meta_value'";
    }
    
  5. Execution: The $sql['where'] array is imploded and appended to the final query string, which is then executed via $wpdb->get_results() without preparation (Line 228).

4. Nonce Acquisition Strategy

REST API exploitation requires a wp_rest nonce sent in the X-WP-Nonce header.

  1. Navigate to Dashboard: Login as a CRM Agent and navigate to /wp-admin/admin.php?page=erp-crm.
  2. Extract Nonce: The ERP plugin localizes several scripts. Use browser_eval to find the nonce in the wpErp or similar global objects.
    • Potential Variable: window.wpErp?.nonce or window.wpCRM?.nonce.
    • Fallback: Standard WordPress REST nonce: browser_eval("wpApiSettings.nonce").

5. Exploitation Strategy

We will perform a Time-Based Blind SQL Injection to verify the vulnerability.

  1. Setup CRM Agent: Ensure a user exists with the crm_agent role.
  2. Baseline Request: Perform a normal GET request to the contacts endpoint to ensure it returns a 200 OK.
    • GET /wp-json/erp/v1/crm/contacts
  3. Payload Construction: Inject a SLEEP() command via the meta_query[meta_key] parameter.
    • Payload: any' OR (SELECT 1 FROM (SELECT SLEEP(5))x) -- -
    • Full URL: /wp-json/erp/v1/crm/contacts?meta_query[meta_key]=any%27%20OR%20(SELECT%201%20FROM%20(SELECT%20SLEEP(5))x)%20--%20-&meta_query[meta_value]=1
  4. Execute Request: Use http_request with the X-WP-Nonce header.
  5. Observe Response Time: If the response takes ~5 seconds, the injection is successful.

6. Test Data Setup

  1. Activate Plugin: Ensure erp is active.
  2. Activate CRM: Ensure the CRM module is enabled via ERP settings.
  3. Create User:
    wp user create attacker attacker@example.com --role=erp_crm_agent --user_pass=password
    
  4. Create Sample Contact: The query needs at least one contact to process logic in some versions.
    # Use WP-CLI to create a contact if possible, or navigate via browser to create one.
    

7. Expected Results

  • Baseline: Response time < 500ms.
  • Exploit: Response time > 5000ms.
  • HTTP Status: 200 OK (the injection is in the WHERE clause of a SELECT query, so it typically returns an empty set or a list of contacts).

8. Verification Steps

After the HTTP exploit, verify the lack of preparation in the source code:

  1. Check for Prepare: grep -A 15 "if ( \$meta_query )" includes/functions-people.php
  2. Confirm Sink: Verify that $wpdb->get_results is called on the $final_query variable without a prepare() wrapper in the same file.

9. Alternative Approaches

If meta_query is not directly exposed via the REST API, test the orderby parameter in the same function:

  • Vulnerable Line: $sql_order_by = "ORDER BY $orderby $order"; (Line 90).
  • Payload: ?orderby=(SELECT 1 FROM (SELECT SLEEP(5))x)

If /erp/v1/crm/contacts is not the correct route, check for:

  • /wp-json/erp/v1/hrm/employees (Requires erp_view_list capability).
  • /wp-json/erp/v1/accounting/v1/customers (Requires accounting access).
    Any endpoint calling erp_get_peoples() will be vulnerable.
Research Findings
Static analysis — not yet PoC-verified

Summary

The ERP plugin for WordPress is vulnerable to SQL Injection via the erp_get_peoples() function due to a lack of parameter preparation and whitelisting. Authenticated attackers with CRM agent permissions can inject arbitrary SQL commands through parameters like meta_query or orderby via REST API endpoints, allowing for sensitive data extraction from the database.

Vulnerable Code

// includes/functions-people.php line 90
$sql_order_by = "ORDER BY $orderby $order";

// ---

// includes/functions-people.php line 96
if ( $meta_query ) {
    $sql['join'][] = "LEFT JOIN $pepmeta_tb as people_meta on people.id = people_meta.`erp_people_id`";

    $meta_key   = isset( $meta_query['meta_key'] ) ? $meta_query['meta_key'] : '';
    $meta_value = isset( $meta_query['meta_value'] ) ? $meta_query['meta_value'] : '';
    $compare    = isset( $meta_query['compare'] ) ? $meta_query['compare'] : '=';

    // INJECTION POINT: $meta_key and $meta_value are concatenated directly
    $sql['where'][] = "AND people_meta.meta_key='$meta_key' and people_meta.meta_value='$meta_value'";
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/erp/1.16.10/includes/functions-people.php	2026-01-01 17:26:14.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/erp/1.16.11/includes/functions-people.php	2026-02-09 12:22:10.000000000 +0000
@@ -61,6 +61,44 @@
     if ( false === $items ) {
         extract( $args );
 
+        // Whitelist allowed orderby columns to prevent SQL injection
+        $allowed_orderby = [
+            'id',
+            'user_id',
+            'first_name',
+            'last_name',
+            'company',
+            'email',
+            'phone',
+            'mobile',
+            'other',
+            'website',
+            'fax',
+            'notes',
+            'street_1',
+            'street_2',
+            'city',
+            'state',
+            'postal_code',
+            'country',
+            'currency',
+            'life_stage',
+            'contact_owner',
+            'hash',
+            'created_by',
+            'created',
+        ];
+
+        if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
+            $orderby = 'id';
+        }
+
+        // Whitelist allowed order directions to prevent SQL injection
+        $order = strtoupper( $order );
+        if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
+            $order = 'DESC';
+        }
+
         $sql         = [];
         $trashed_sql = $trashed ? '`deleted_at` is not null' : '`deleted_at` is null';

Exploit Outline

The exploit targets the erp_get_peoples() function, which is utilized by several REST API routes. An attacker needs an account with at least 'crm_agent' permissions. 1. Authenticate as a CRM Agent and retrieve a REST API nonce (X-WP-Nonce) from the WordPress dashboard (often located in the wpErp or wpApiSettings JavaScript globals). 2. Send a GET request to a vulnerable REST route, such as `/wp-json/erp/v1/crm/contacts`. 3. Include a payload in the `meta_query[meta_key]` or `orderby` parameters. For example, setting `meta_query[meta_key]` to `any' OR (SELECT 1 FROM (SELECT SLEEP(5))x) -- -` triggers a time-based blind SQL injection. 4. Observe the response latency; a significant delay (e.g., 5 seconds) confirms the vulnerability and successful execution of the injected SQL.

Check if your site is affected.

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