Ultra Addons for Contact Form 7 <= 3.5.36 - Authenticated (Contributor+) Stored Cross-Site Scripting
Description
The Ultra Addons for Contact Form 7 plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 3.5.36 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
<=3.5.36What Changed in the Fix
Changes introduced in v3.5.37
Source Code
WordPress.org SVN# Research Plan: Ultra Addons for Contact Form 7 Stored XSS (CVE-2026-32460) ## 1. Vulnerability Summary The **Ultra Addons for Contact Form 7** plugin (up to 3.5.36) contains a stored cross-site scripting (XSS) vulnerability in its option import functionality. The vulnerability exists because the …
Show full research plan
Research Plan: Ultra Addons for Contact Form 7 Stored XSS (CVE-2026-32460)
1. Vulnerability Summary
The Ultra Addons for Contact Form 7 plugin (up to 3.5.36) contains a stored cross-site scripting (XSS) vulnerability in its option import functionality. The vulnerability exists because the AJAX handler uacf7_option_import_callback fails to properly validate the user's capabilities (due to an incorrect use of is_admin()) and subsequently stores unsanitized JSON data into post meta. This data is then rendered on the frontend or backend without sufficient escaping.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
uacf7_option_import(hooked viawp_ajax_uacf7_option_import) - Vulnerable Parameter:
tf_import_option(JSON string) - Required Authentication: Contributor or higher.
- Privilege Escalation Mechanism: The plugin attempts to restrict the action to administrators but uses the following check:
In the context of an AJAX request (if ( $current_user_role !== 'administrator' && ! is_admin() ) { wp_die( 'You do not have sufficient permissions to access this page.' ); }admin-ajax.php),is_admin()always returnstrue. Therefore, the condition$current_user_role !== 'administrator' && falsealways evaluates tofalse, allowing any authenticated user with access to the admin dashboard (like a Contributor) to bypass the check.
3. Code Flow
- Entry Point:
UACF7_Options::__construct()registers the AJAX action:add_action( 'wp_ajax_uacf7_option_import', array( $this, 'uacf7_option_import_callback' ) ); - Nonce Verification:
uacf7_option_import_callback()verifies the noncetf_options_nonce. - Authorization Bypass: The function performs the flawed role check described above.
- Data Processing:
$imported_datais created byjson_decode( wp_unslash( trim( $_POST['tf_import_option'] ) ), true );.$form_idis retrieved from$_POST['form_id'].
- Sink: The unsanitized array is saved to the database:
update_post_meta( $form_id, 'uacf7_form_opt', $imported_data ); - Rendering: When the contact form (ID:
$form_id) is rendered, the plugin retrievesuacf7_form_optand outputs values (likely in a style block or wrapper div) without proper escaping.
4. Nonce Acquisition Strategy
The nonce tf_options_nonce is localized into the JavaScript variable tf_options_params. This occurs in admin/tf-options/TF_Options.php within the tf_options_admin_enqueue_scripts method.
Strategy:
- Log in as a Contributor.
- The script is enqueued on several admin screens, including the Contact Form 7 list page (
toplevel_page_wpcf7). - Navigate to
/wp-admin/admin.php?page=wpcf7. - Execute
browser_evalto extract the nonce:browser_eval("window.tf_options_params?.tf_options_nonce")
5. Exploitation Strategy
- Preparation: Identify an existing Contact Form 7 form ID (e.g.,
123). If none exist, create one. - Payload Crafting: Create a JSON object where values contain XSS payloads. Since this meta is for "Form Options" (styling), common fields likely include font families or custom CSS.
payload = {"uacf7_custom_css": "</style><script>alert(document.domain)</script>"}
- Request: Send a POST request to
admin-ajax.php.- Action:
uacf7_option_import - ajax_nonce: [Extracted Nonce]
- form_id: 123
- tf_import_option:
{"any_key": "<img src=x onerror=alert(1)>"}
- Action:
- Verification: View the page where Contact Form 123 is embedded.
6. Test Data Setup
- Install Plugins: Contact Form 7 and Ultra Addons for Contact Form 7 (v3.5.36).
- Create User: A user with the
contributorrole. - Create Form: A Contact Form 7 form. Note its ID (e.g.,
wp-post-createor check the UI). - Create Page: A public page containing the shortcode:
[contact-form-7 id="123" title="Test Form"].
7. Expected Results
- The AJAX request should return a JSON success message:
{"success":true,"data":{"status":"success","message":"Options imported successfully!"}}. - The database should show the payload stored in the
wp_postmetatable for the specifiedpost_idunder the keyuacf7_form_opt. - When visiting the public page, an alert box should trigger.
8. Verification Steps (WP-CLI)
Check if the meta was successfully injected:
wp post meta get [FORM_ID] uacf7_form_opt --format=json
This should return the JSON object containing the <script> or <img> tag.
9. Alternative Approaches
If the tf_options_nonce is not localized on the main Contact Form 7 page for Contributors, try navigating to:
/wp-admin/admin.php?page=uacf7_settings/wp-admin/admin.php?page=uacf7_setup-wizard
If a specific key is required in the JSON for it to be rendered, analyze admin/tf-options/metaboxes/ or options/ files to find valid option keys used by the plugin (e.g., form-title-color, form-background-color). A reliable fallback is to inject into every key:{"key1": "<script>alert(1)</script>", "key2": "<script>alert(1)</script>" ...}
Summary
The Ultra Addons for Contact Form 7 plugin is vulnerable to Stored Cross-Site Scripting via the 'uacf7_option_import' AJAX action due to a flawed capability check and lack of output escaping. Authenticated attackers with Contributor-level access can bypass the intended administrator-only restriction and inject malicious scripts into contact form metadata, which then executes when the form is viewed.
Vulnerable Code
// admin/tf-options/TF_Options.php:64 public function uacf7_option_import_callback() { if ( ! isset( $_POST['ajax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ajax_nonce'] ) ), 'tf_options_nonce' ) ) { return; } // Checked Currenct can save option $current_user = wp_get_current_user(); $current_user_role = $current_user->roles[0]; if ( $current_user_role !== 'administrator' && ! is_admin() ) { wp_die( 'You do not have sufficient permissions to access this page.' ); } $imported_data = json_decode( wp_unslash( trim( $_POST['tf_import_option'] ) ), true ); $form_id = stripslashes( $_POST['form_id'] ); $response = [ 'status' => 'error', 'message' => __( 'Something went wrong!', 'ultimate-addons-cf7' ), ]; if ( ! empty( $imported_data ) && is_array( $imported_data ) ) { if ( $form_id != 0 ) { update_post_meta( $form_id, 'uacf7_form_opt', $imported_data ); }
Security Fix
@@ -64,12 +64,10 @@ return; } - // Checked Currenct can save option - $current_user = wp_get_current_user(); - $current_user_role = $current_user->roles[0]; - - if ( $current_user_role !== 'administrator' && ! is_admin() ) { - wp_die( 'You do not have sufficient permissions to access this page.' ); + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( + [ 'message' => __( 'You do not have sufficient permissions to access this page.', 'ultimate-addons-cf7' ) ] + ); } $imported_data = json_decode( wp_unslash( trim( $_POST['tf_import_option'] ) ), true );
Exploit Outline
The exploit targets the 'uacf7_option_import' AJAX action. An authenticated user (Contributor+) can bypass the permission check because it incorrectly uses is_admin() within an AJAX context, where it always returns true. The attacker extracts the 'tf_options_nonce' from the WordPress admin dashboard scripts and sends a POST request to admin-ajax.php. This request includes a 'tf_import_option' JSON payload containing a malicious script and a target 'form_id'. The plugin saves this unsanitized JSON data into the 'uacf7_form_opt' post meta, which triggers the XSS payload whenever the associated contact form is rendered on the site.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.