CVE-2026-57369

Themify Builder <= 7.7.4 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
7.7.5
Patched in
8d
Time to patch

Description

The Themify Builder plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 7.7.4 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers 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:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=7.7.4
PublishedJuly 7, 2026
Last updatedJuly 14, 2026
Affected pluginthemify-builder

What Changed in the Fix

Changes introduced in v7.7.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - Themify Builder <= 7.7.4 Unauthenticated Stored XSS ## 1. Vulnerability Summary The **Themify Builder** plugin (versions up to 7.7.4) contains an unauthenticated stored cross-site scripting vulnerability. The flaw exists in the handling of the `tb_generate_on_fly` AJA…

Show full research plan

Exploitation Research Plan - Themify Builder <= 7.7.4 Unauthenticated Stored XSS

1. Vulnerability Summary

The Themify Builder plugin (versions up to 7.7.4) contains an unauthenticated stored cross-site scripting vulnerability. The flaw exists in the handling of the tb_generate_on_fly AJAX action, which is intended to regenerate CSS stylesheets for Builder-enabled posts. This action is registered for unauthenticated users (wp_ajax_nopriv_) and fails to properly validate the user's authority to modify post data when a data parameter is supplied. Consequently, an attacker can overwrite the Builder layout JSON for any post, injecting malicious scripts into module settings that are later rendered without sufficient escaping.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: tb_generate_on_fly (Unauthenticated)
  • Vulnerable Parameter: data (POST request)
  • Authentication: None (Unauthenticated)
  • Preconditions:
    • The target post must have Themify Builder enabled.
    • A valid tf_nonce must be obtained from the frontend.

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with action=tb_generate_on_fly.
  2. Registration: In classes/class-themify-builder-stylesheet.php, the action is hooked:
    add_action('wp_ajax_nopriv_tb_generate_on_fly', array(__CLASS__, 'save_builder_css'), 10);
    
  3. Vulnerable Function: Themify_Builder_Stylesheet::save_builder_css() is executed.
  4. Inadequate Authorization: The function checks for a valid nonce (tf_nonce) but lacks a current_user_can('edit_posts') check.
  5. Sink: The function processes $_POST['id'] (Post ID) and $_POST['data'] (Builder JSON). It updates the post meta (typically _themify_builder_settings_json) with the attacker-controlled data.
  6. Execution: When a victim views the modified post, the Builder's rendering engine (e.g., in Themify_Builder_Component_Module) processes the malicious JSON. If a module like text contains a script in its content_text setting, it is echoed to the page, triggering XSS.

4. Nonce Acquisition Strategy

The tb_generate_on_fly action requires a nonce named tf_nonce. This nonce is generated and localized in the frontend whenever a Builder-enabled page is loaded.

Extraction Steps:

  1. Identify a page or post where Themify Builder is active (e.g., the homepage or a sample post).
  2. Navigate to the page using the browser_navigate tool.
  3. The plugin enqueues the tb_builder_js_style script, which localizes the nonce into the ThemifyBuilderStyle global object.
  4. Execute the following JavaScript via browser_eval:
    window.ThemifyBuilderStyle?.nonce
    

5. Exploitation Strategy

The goal is to overwrite a post's content with a "Text" module containing an XSS payload.

Step 1: Obtain Nonce

  • Use the browser_navigate tool to go to a Builder-enabled post.
  • Use browser_eval to extract window.ThemifyBuilderStyle.nonce.

Step 2: Prepare Payload
Themify Builder expects a JSON array of rows. We will inject a script into the content_text setting of a text module.

[
  {
    "styling": {},
    "cols": [
      {
        "styling": {},
        "modules": [
          {
            "mod_name": "text",
            "mod_settings": {
              "content_text": "<script>alert(document.domain)</script>"
            },
            "styling": {}
          }
        ]
      }
    ]
  }
]

Step 3: Execute Injection
Send the following POST request using the http_request tool:

  • URL: http://[target-ip]/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    • action: tb_generate_on_fly
    • id: [Target_Post_ID]
    • nonce: [Extracted_tf_nonce]
    • data: [URL_Encoded_JSON_Payload]

6. Test Data Setup

  1. Create a target post:
    wp post create --post_type=post --post_title="Vulnerable Page" --post_status=publish
    
  2. Enable Builder on the post:
    Themify Builder usually initializes when a post is edited in the builder. If testing manually, ensure at least one row exists. For automation, identify the ID of the post created.

7. Expected Results

  • The AJAX request should return a success indicator (often a JSON response or 1).
  • Upon navigating to the post (/?p=[ID]), a JavaScript alert containing the document domain should appear.

8. Verification Steps

  1. Verify Post Meta: Check if the builder settings were successfully overwritten via WP-CLI:
    wp post meta get [Post_ID] _themify_builder_settings_json
    
  2. Verify Web Content: Fetch the post and grep for the payload:
    # Use http_request to fetch the page and check the response body
    

9. Alternative Approaches

  • Different Modules: If the text module is sanitized in some environments, try the heading module or image module (injecting into the caption or alt fields).
  • Global Styles Vector: If the post ID is restricted, explore if action=tb_save_css or action=tb_slider_live_styling in class-themify-builder-stylesheet.php also lack authentication checks, as they process similar styling/data inputs.
  • Bypassing Nonce: Check if the tb_generate_on_fly action functions if the nonce parameter is omitted or if a different public nonce (like wp_rest) is provided, though check_ajax_referer usually prevents this.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Themify Builder plugin for WordPress is vulnerable to unauthenticated stored cross-site scripting due to a lack of authorization checks in the `tb_generate_on_fly` AJAX action. An attacker can obtain a public nonce from the frontend and overwrite the builder layout for any public post with a malicious JSON payload, leading to script execution when the page is viewed.

Vulnerable Code

// classes/class-themify-builder-stylesheet.php:12
public static function init() {
    if (themify_is_ajax()) {
        add_action('wp_ajax_tb_slider_live_styling', array(__CLASS__, 'slider_live_styling'), 10);
        add_action('wp_ajax_nopriv_tb_generate_on_fly', array(__CLASS__, 'save_builder_css'), 10);
        add_action('wp_ajax_tb_generate_on_fly', array(__CLASS__, 'save_builder_css'), 10);
        // ...
    }
}

---

// classes/class-themify-builder-stylesheet.php:161
public static function save_builder_css(bool $echo = false) {
    check_ajax_referer('tf_nonce', 'nonce');
    if (!empty($_POST['bid'])) {
        $id = (int) $_POST['bid'];

        // Security: non-logged-in visitors may only write CSS for posts that
        // are published, publicly queryable, and not password-protected.
        if ( ! is_user_logged_in() ) {
            $post = get_post( $id );
            if ( ! $post ) {
                wp_die();
            }
            $post_type_obj = get_post_type_object( $post->post_type );
            if (
                $post->post_status !== 'publish'
                || post_password_required( $post )
                || empty( $post_type_obj->public )
            ) {
                wp_die();
            }
        }
        // ... (truncated: processes $_POST['css'] or $_FILES['css'] into $data)
        if (isset($data)) {
            if (is_string($data)) {
                $data = json_decode($data, true);
            } elseif (!is_array($data)) {
                $data = array();
            }
            $res = self::write_stylesheet($id, $data, !empty($_POST['custom_css']) ? stripcslashes($_POST['custom_css']) : '');
            // ...
        }
    }
    wp_die();
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/themify-builder/7.7.4/classes/class-themify-builder-active.php /home/deploy/wp-safety.org/data/plugin-versions/themify-builder/7.7.5/classes/class-themify-builder-active.php
--- /home/deploy/wp-safety.org/data/plugin-versions/themify-builder/7.7.4/classes/class-themify-builder-active.php	2026-06-03 19:11:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/themify-builder/7.7.5/classes/class-themify-builder-active.php	2026-06-16 03:32:56.000000000 +0000
@@ -298,7 +298,18 @@
             return $attr;
         }
 
+        private static function can_edit_builder_post( $post_id = 0 ): bool {
+            if ( ! current_user_can( 'edit_posts' ) ) {
+                return false;
+            }
+            $post_id = (int) $post_id;
+            return $post_id > 0 ? current_user_can( 'edit_post', $post_id ) : true;
+        }
+
         public static function load_editor() {
+            if ( ! self::can_edit_builder_post() ) {
+                wp_die( -1, 403 );
+            }
             global $wp_scripts, $wp_styles, $concatenate_scripts, $wp_actions;
@@ -386,9 +397,12 @@
          */
         public static function load_module_partial_ajaxify() {
             check_ajax_referer('tf_nonce', 'nonce');
+            if ( ! self::can_edit_builder_post( $_POST['bid'] ?? 0 ) ) {
+                wp_die( -1, 403 );
+            }
             themify_disable_other_lazy();
             Themify_Builder::$frontedit_active = true;
-            Themify_Builder::$builder_active_id = $_POST['bid'];
+            Themify_Builder::$builder_active_id = (int) $_POST['bid'];
             $new_modules = apply_filters('themify_builder_load_module_partial', array(
                 'mod_name' => $_POST['tb_module_slug'],
                 'mod_settings' => json_decode(stripslashes($_POST['tb_module_data']), true),

Exploit Outline

1. Nonce Acquisition: Browse a public page where Themify Builder is active. Extract the `tf_nonce` value from the localized JavaScript object `ThemifyBuilderStyle`. 2. Target Identification: Identify a published Post ID (`bid`) that has the builder enabled. 3. Payload Creation: Construct a malicious JSON payload representing a Builder layout. For example, a 'text' module with a 'content_text' setting containing `<script>alert(document.domain)</script>`. 4. Injection: Send an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the parameters `action=tb_generate_on_fly`, `nonce=[EXTRACTED_NONCE]`, `bid=[POST_ID]`, and `css=[MALICIOUS_JSON]`. 5. Execution: Access the target post in a browser. The plugin will render the modified layout settings and execute the injected script.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.