CVE-2026-6127

Elementor Website Builder <= 4.0.4 - Authenticated (Contributor+) Stored Cross-Site Scripting via REST API

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

Description

The Elementor Website Builder plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the _elementor_data meta field in versions up to, and including, 4.0.4. This is due to insufficient input sanitization when processing form-encoded REST API requests. The plugin registers the _elementor_data meta field with show_in_rest but omits a sanitize_callback, relying instead on a rest_pre_insert_post filter (sanitize_post_data function) that only sanitizes JSON-encoded request bodies. When a contributor sends a form-encoded PATCH request to the WordPress REST API, the json_decode() call on the raw body returns null, causing all sanitization to be skipped. The unsanitized data is then stored via update_post_meta() and later output without escaping through multiple widget sinks including the HTML widget's print_unescaped_setting() function. 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<=4.0.4
PublishedApril 30, 2026
Last updatedMay 1, 2026
Affected pluginelementor

What Changed in the Fix

Changes introduced in v4.0.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to demonstrate the Stored Cross-Site Scripting (XSS) vulnerability in the Elementor Website Builder plugin (<= 4.0.4). ### 1. Vulnerability Summary The vulnerability exists because Elementor registers the `_elementor_data` meta field for use in the Wor…

Show full research plan

This research plan outlines the steps required to demonstrate the Stored Cross-Site Scripting (XSS) vulnerability in the Elementor Website Builder plugin (<= 4.0.4).

1. Vulnerability Summary

The vulnerability exists because Elementor registers the _elementor_data meta field for use in the WordPress REST API but fails to provide a sanitize_callback during registration. Instead, it relies on a custom filter hooked to rest_pre_insert_post (and rest_pre_insert_page) named sanitize_post_data.

This filter attempts to decode the raw request body as JSON to sanitize the Elementor layout data. However, if an attacker sends a request with Content-Type: application/x-www-form-urlencoded, the json_decode() call on the raw body returns null. The function then exits early without performing any sanitization, allowing unsanitized JSON (containing XSS payloads) to be stored in the _elementor_data meta field.

2. Attack Vector Analysis

  • Endpoint: /wp-json/wp/v2/posts/<ID> or /wp-json/wp/v2/pages/<ID>
  • Method: POST (with _method=PATCH) or PATCH
  • Authentication: Authenticated (Contributor level or higher).
  • Vulnerable Parameter: meta[_elementor_data]
  • Content-Type: application/x-www-form-urlencoded
  • Payload Sink: The HTML widget within Elementor, which outputs data via print_unescaped_setting().

3. Code Flow

  1. Entry Point: An authenticated user sends a PATCH request to the WordPress REST API to update a post they have permission to edit.
  2. Meta Registration: Elementor calls register_meta( 'post', '_elementor_data', ... ) with 'show_in_rest' => true, but without a 'sanitize_callback'.
  3. Vulnerable Filter: The rest_pre_insert_post filter triggers Elementor's sanitize_post_data function.
  4. The Bypass:
    • sanitize_post_data calls json_decode( $request->get_body() ).
    • Because the request is application/x-www-form-urlencoded, the raw body is a string like meta%5B_elementor_data%5D=..., which is not valid JSON.
    • json_decode returns null.
    • The function returns the $prepared_post data unchanged.
  5. Storage: WordPress core processes the meta array in the request and updates the _elementor_data post meta using update_post_meta() because it is registered as show_in_rest.
  6. Rendering: When a user views the post, Elementor parses _elementor_data. The HTML widget processes the html setting and renders it directly to the page without escaping.

4. Nonce Acquisition Strategy

The WordPress REST API requires a wp_rest nonce for authenticated requests.

  1. Identify Trigger: The REST nonce is globally available in the WordPress admin dashboard for logged-in users.
  2. Access Strategy:
    • Log in as a Contributor.
    • Navigate to the WordPress Dashboard (/wp-admin/).
    • Use browser_eval to extract the nonce from the wpApiSettings object.
  3. JavaScript Path: window.wpApiSettings.nonce

5. Exploitation Strategy

  1. Preparation: Identify or create a post/page ID where the Contributor is the author.
  2. Payload Construction: Create a JSON string for _elementor_data that defines an HTML widget containing the XSS payload.
    [{"id":"poc_id","elType":"widget","widgetType":"html","settings":{"html":"<script>alert(document.domain)</script>"},"elements":[]}]
    
  3. Request Execution: Use the http_request tool to send a POST request to the REST API with the _method=PATCH override.
    • URL: https://<target>/wp-json/wp/v2/posts/<ID>
    • Headers:
      • X-WP-Nonce: <extracted_nonce>
      • Content-Type: application/x-www-form-urlencoded
    • Body:
      meta[_elementor_data]=[{"id":"xss","elType":"widget","widgetType":"html","settings":{"html":"<script>alert(document.domain)</script>"},"elements":[]}]&meta[_elementor_edit_mode]=builder

6. Test Data Setup

  1. User: Create a user with the contributor role.
  2. Content: As the contributor, create a draft post:
    wp post create --post_type=post --post_title='XSS Test' --post_status=draft --post_author=<user_id>
  3. Permissions: Ensure the contributor can edit this specific post ID.

7. Expected Results

  • The REST API should return a 200 OK response with the updated post data.
  • The _elementor_data field in the database should contain the raw, unsanitized JSON string with the <script> tag.
  • When navigating to the post URL (or previewing it), a browser alert box should appear showing the document domain.

8. Verification Steps

  1. Database Check: Use WP-CLI to verify the stored meta value:
    wp post meta get <ID> _elementor_data
    Confirmation: The output should contain the raw <script> payload.
  2. Frontend Check: Use browser_navigate to visit the post URL and check for the presence of the script in the DOM or the execution of the alert.

9. Alternative Approaches

If the standard posts endpoint is restricted, try the pages endpoint:

  • Endpoint: /wp-json/wp/v2/pages/<ID>
  • Payload Modification: If the HTML widget is somehow blocked, attempt to use the text-editor widget or the heading widget, though these may have more internal sanitization than the html widget.
  • JSON in Form Body: If the meta parameter is ignored, try sending the payload as a single url-encoded parameter: _elementor_data=<payload> (though standard REST API updates specifically look for the meta key).
Research Findings
Static analysis — not yet PoC-verified

Summary

Elementor Website Builder (<= 4.0.4) is vulnerable to Stored Cross-Site Scripting via the _elementor_data meta field. Authenticated contributors can bypass the plugin's custom REST API sanitization filter by sending form-encoded requests, which causes a JSON decoding failure that allows unsanitized payloads to be stored and subsequently executed in the context of other users.

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.4/app/modules/import-export-customization/data/routes/process-media.php /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.5/app/modules/import-export-customization/data/routes/process-media.php
--- /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.4/app/modules/import-export-customization/data/routes/process-media.php	2025-10-27 10:18:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.5/app/modules/import-export-customization/data/routes/process-media.php	2026-04-30 11:19:30.000000000 +0000
@@ -31,6 +31,10 @@
 
 		$cloud_kit_library_app = $this->get_cloud_kit_library_app();
 
+		if ( $cloud_kit_library_app && ! $cloud_kit_library_app->is_connected() ) {
+			return Response::error( ImportExportCustomizationModule::MEDIA_PROCESSING_ERROR, 'Cloud Library is not connected' );
+		}
+
 		$media_urls = $request->get_param( 'media_urls' );
 		$kit = $request->get_param( 'kit' );
 		$quota = null;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.4/assets/js/admin-modules.strings.js /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.5/assets/js/admin-modules.strings.js
--- /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.4/assets/js/admin-modules.strings.js	2026-04-28 08:44:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/elementor/4.0.5/assets/js/admin-modules.strings.js	2026-04-30 11:19:30.000000000 +0000
@@ -23,6 +23,6 @@
 __( 'Submit & Deactivate', 'elementor' );
 __( 'Skip & Deactivate', 'elementor' );
 __( 'New Template', 'elementor' );
+__( 'New Floating Elements', 'elementor' );
 __( 'Sign Up', 'elementor' );
-__( 'Don\'t Show Again', 'elementor' );
-__( 'New Floating Elements', 'elementor' );
\ No newline at end of file
+__( 'Don\'t Show Again', 'elementor' );
\ No newline at end of file
... (truncated)

Exploit Outline

1. Authenticate as a user with Contributor level permissions or higher. 2. Obtain a valid WordPress REST API nonce (found in the `wpApiSettings` JavaScript object on the dashboard). 3. Identify a post or page ID that the contributor has permission to edit. 4. Construct a malicious JSON payload for the `_elementor_data` meta field. This payload should define an HTML widget containing a Cross-Site Scripting (XSS) script in its 'html' setting: `[{"id":"xss","elType":"widget","widgetType":"html","settings":{"html":"<script>alert(document.domain)</script>"},"elements":[]}]`. 5. Send a `PATCH` (or `POST` with `_method=PATCH`) request to the WordPress REST API endpoint `/wp-json/wp/v2/posts/<ID>`. 6. Set the `Content-Type` header to `application/x-www-form-urlencoded`. 7. Include the malicious layout in the `meta[_elementor_data]` parameter. By using form-encoding, the attacker causes the plugin's `rest_pre_insert_post` filter to fail its `json_decode()` attempt on the raw body, which results in the sanitization logic being bypassed. 8. Once the request is successful, navigate to the modified post. The unsanitized script will execute via the Elementor HTML widget's unescaped output sink.

Check if your site is affected.

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