CVE-2026-32535

JS Help Desk – AI-Powered Support & Ticketing System <= 3.0.3 - Authenticated (Subscriber+) Insecure Direct Object Reference

mediumAuthorization Bypass Through User-Controlled Key
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.0.4
Patched in
11d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.0.3
PublishedMarch 23, 2026
Last updatedApril 2, 2026
Affected pluginjs-support-ticket

What Changed in the Fix

Changes introduced in v3.0.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

```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_
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.3/modules/reply/model.php /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/reply/model.php
--- /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.3/modules/reply/model.php	2026-02-03 04:22:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/reply/model.php	2026-02-13 04:32:24.000000000 +0000
@@ -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.*
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.3/modules/ticket/model.php /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/ticket/model.php
--- /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.3/modules/ticket/model.php	2026-02-03 04:22:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/js-support-ticket/3.0.4/modules/ticket/model.php	2026-02-13 04:32:24.000000000 +0000
@@ -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.