Themify Builder <= 7.7.4 - Unauthenticated Stored Cross-Site Scripting
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:NTechnical Details
<=7.7.4What Changed in the Fix
Changes introduced in v7.7.5
Source Code
WordPress.org SVN# 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_noncemust be obtained from the frontend.
3. Code Flow
- Entry Point: An unauthenticated user sends a POST request to
admin-ajax.phpwithaction=tb_generate_on_fly. - 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); - Vulnerable Function:
Themify_Builder_Stylesheet::save_builder_css()is executed. - Inadequate Authorization: The function checks for a valid nonce (
tf_nonce) but lacks acurrent_user_can('edit_posts')check. - 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-controlleddata. - 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 liketextcontains a script in itscontent_textsetting, 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:
- Identify a page or post where Themify Builder is active (e.g., the homepage or a sample post).
- Navigate to the page using the
browser_navigatetool. - The plugin enqueues the
tb_builder_js_stylescript, which localizes the nonce into theThemifyBuilderStyleglobal object. - 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_navigatetool to go to a Builder-enabled post. - Use
browser_evalto extractwindow.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_flyid:[Target_Post_ID]nonce:[Extracted_tf_nonce]data:[URL_Encoded_JSON_Payload]
6. Test Data Setup
- Create a target post:
wp post create --post_type=post --post_title="Vulnerable Page" --post_status=publish - 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
- Verify Post Meta: Check if the builder settings were successfully overwritten via WP-CLI:
wp post meta get [Post_ID] _themify_builder_settings_json - 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
textmodule is sanitized in some environments, try theheadingmodule orimagemodule (injecting into thecaptionoraltfields). - Global Styles Vector: If the post ID is restricted, explore if
action=tb_save_cssoraction=tb_slider_live_stylinginclass-themify-builder-stylesheet.phpalso lack authentication checks, as they process similar styling/data inputs. - Bypassing Nonce: Check if the
tb_generate_on_flyaction functions if thenonceparameter is omitted or if a different public nonce (likewp_rest) is provided, thoughcheck_ajax_refererusually prevents this.
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
@@ -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.