JS Help Desk – AI-Powered Support & Ticketing System <= 3.0.3 - Authenticated (Subscriber+) Insecure Direct Object Reference
Description
The JS Help Desk – AI-Powered Support & Ticketing System plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 3.0.3 due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform unauthorized actions.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=3.0.3What Changed in the Fix
Changes introduced in v3.0.4
Source Code
WordPress.org SVN```markdown # Exploitation Research Plan: CVE-2026-32535 (JS Help Desk IDOR) ## 1. Vulnerability Summary The **JS Help Desk – AI-Powered Support & Ticketing System** plugin (up to version 3.0.3) is vulnerable to an **Insecure Direct Object Reference (IDOR)**. The vulnerability exists in the handlin…
Show full research plan
# Exploitation Research Plan: CVE-2026-32535 (JS Help Desk IDOR)
## 1. Vulnerability Summary
The **JS Help Desk – AI-Powered Support & Ticketing System** plugin (up to version 3.0.3) is vulnerable to an **Insecure Direct Object Reference (IDOR)**. The vulnerability exists in the handling of ticket-related tasks (such as closing or deleting a ticket) where the system fails to verify if the authenticated user performing the action is the owner of the object (the ticket) specified by a user-controlled identifier (`ticketid`).
While the plugin uses a random hash for some public links to prevent unauthorized access, many internal operations rely on the raw integer ID (`ticketid`) and fail to validate ownership, allowing a Subscriber-level user to perform actions on tickets belonging to other users.
## 2. Attack Vector Analysis
- **Endpoint**: Front-end Control Panel or `admin-ajax.php`.
- **Target Action**: `closeticket` or `deleteticket`.
- **Vulnerable Parameter**: `ticketid` (integer).
- **Authentication**: Authenticated (Subscriber or higher).
- **Preconditions**:
- The plugin must be active.
- At least two users exist (Victim and Attacker).
- The Attacker must be able to discover or guess the Victim's `ticketid` (usually sequential integers).
## 3. Code Flow
1. The request enters via `the_
Summary
The JS Help Desk plugin for WordPress is vulnerable to Insecure Direct Object Reference (IDOR) in versions up to 3.0.3. This occurs because the plugin fails to verify if the authenticated user has ownership of the ticket specified by user-controlled parameters like 'ticket_id' or 'ticketId' in functions handling ticket replies and AI-powered support features.
Vulnerable Code
// modules/reply/model.php around line 510 function get_filtered_replies() { // Verify nonce check_ajax_referer('get-filtered-replies', '_wpnonce'); $jsst_ticket_id = intval(JSSTrequest::getVar('ticket_id')); if (!$jsst_ticket_id) { wp_send_json_error(['message' => __('Ticket ID is required.', 'js-support-ticket')]); } $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_query = " SELECT r.* FROM `" . jssupportticket::$_db->prefix . "js_ticket_replies` AS r WHERE r.ticketid = " . esc_sql($jsst_ticket_id) . " --- // modules/ticket/model.php around line 3018 $jsst_id = JSSTrequest::getVar('ticketId'); $jsst_subject = JSSTrequest::getVar('ticketSubject'); $jsst_agentquery = ""; // ... (missing ownership validation for regular users) $jsst_query = "SELECT ticket.message FROM `" . jssupportticket::$_db->prefix . "js_ticket_tickets` AS ticket WHERE ticket.id = " . esc_sql($jsst_id);
Security Fix
@@ -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);
Exploit Outline
The exploit targets AJAX endpoints or internal plugin controllers that handle ticket operations. 1. **Authentication**: The attacker logs in as a Subscriber-level user. 2. **Discovery**: The attacker identifies the `ticket_id` (integer) of a victim's ticket. These are often sequential and easy to guess. 3. **Request**: The attacker sends a request to an endpoint like `wp-admin/admin-ajax.php` with the `action` parameter set to a vulnerable task (e.g., `get-filtered-replies`). 4. **Payload**: The payload includes the victim's `ticket_id` and a valid security nonce (which is often available in the HTML of the user's own control panel). 5. **Bypass**: Because the vulnerable version only checks for a valid integer ID and a general nonce, but fails to check if the `uid` of the ticket matches the `uid` of the current requester, the system executes the action (e.g., returning sensitive ticket replies or modifying ticket status) regardless of ownership.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.