The Exploit
An unauthenticated attacker can inject arbitrary PHP callbacks into Kali Forms' placeholder processing pipeline by submitting crafted POST field names that override legitimate internal placeholder handlers.
POST /wp-admin/admin-ajax.php?action=kali_form_process HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
form_id=1&{entryCounter}=system('id')&{thisPermalink}=phpinfo&submit=1
On vulnerable versions, the server processes these field names as placeholder keys. Because the code fails to restore the legitimate internal callable placeholders before returning, the attacker-supplied values replace the safe callback references. When the form processor later invokes call_user_func() on these placeholders, it executes the attacker's payload. The response will contain command output (if the callback produces output) or the attacker observes side effects such as files written to disk or database records modified by the injected function call.
What the Patch Did
Before:
foreach ($this->data as $key => $value) {
$type = 'textbox';
if (isset($this->field_type_map[$key]) && !empty($this->field_type_map[$key])) {
$type = $this->field_type_map[$key];
}
$this->_run_placeholder_switch($type, $key, $value);
}
$this->placeholdered_data = apply_filters($this->slug . '_form_placeholders', $this->placeholdered_data);
return $data;
After:
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 (!empty($this->field_type_map[$key])) {
$type = $this->field_type_map[$key];
}
$this->_run_placeholder_switch($type, $key, $value);
}
$this->placeholdered_data = apply_filters($this->slug . '_form_placeholders', $this->placeholdered_data);
$this->restore_internal_callable_placeholders();
return $data;
The patch introduced two critical security controls. First, it added explicit field-map validation via array_key_exists() in the loop, ensuring that only fields registered in the form definition are processed—unknown POST keys are silently skipped. Second, and most importantly, it calls $this->restore_internal_callable_placeholders() immediately before returning, which overwrites the $this->placeholdered_data array with the legitimate internal callback references. This prevents user-supplied POST data from poisoning the placeholder handlers that call_user_func() will later invoke.
Root Cause
CWE-94: Improper Control of Generation of Code ('Code Injection') combined with CWE-471: Modification of Assumed-Immutable Data.
The prepare_post_data() function accepts user-supplied field names from $_POST and directly merges them into the $this->placeholdered_data array without sanitization or validation. Because the code uses generic key-matching (checking if a POST key exists), an attacker can submit field names identical to reserved internal placeholder keys like {entryCounter}, {thisPermalink}, or {submission_link}. These keys are expected to map to safe internal callback functions (such as WordPress core functions), but the code never restores or protects these mappings after filters run. When call_user_func() later processes the placeholders, it executes whatever callback string the attacker placed in the POST data, crossing the trust boundary between user input and executable code without any check.
Why It Works
The load-bearing line is $this->restore_internal_callable_placeholders();. Without it, the legitimate callback references stored in the placeholder array remain poisoned by user input. Removing this single call would leave the vulnerability fully exploitable—an attacker's injected callback would persist through to the call_user_func() sink.
The field-map validation (array_key_exists() check) is necessary but insufficient. It prevents processing of completely unknown field names, but an attacker could still craft field names matching legitimate form fields and exploit those—unless they were also constrained. The validation layer raises the bar for exploitation but only the placeholder restoration provides defense-in-depth: it guarantees that no matter what keys reach the placeholder dictionary, the dangerous keys always resolve to safe, immutable callbacks before they are invoked. The engineer added the field-map initialization check (setup_field_map() call) to ensure the map is populated early, preventing a race condition where an empty map would allow all keys to pass through.
Hardening Checklist
-
Whitelist field keys before processing: Use
array_key_exists()orisset()against an explicit whitelist of expected form field names; reject all others. Do not rely onisset()alone, as it treats missing keys and null values identically (CWE-471). -
Separate user data from callable references: Store form field values in one array and callback references in another, never in the same structure. Validate that the callback dictionary is always populated from a hard-coded or database source, not user input.
-
Restore immutable state before use in dynamic calls: Immediately before invoking
call_user_func(),array_map(), or any function that uses user-touched data as a callback, reload the callback from a trusted source. Do not assume a single initialization is sufficient. -
Use a callback registry pattern: Instead of storing callback strings in user-influenced arrays, maintain a separate registry mapping field names to closures or static class methods. Look up callbacks only in this registry.
-
Validate callback targets with
is_callable(): Before invoking any user-derived callback, confirm it is callable and log the invocation. This will catch poisoned or malformed callbacks at runtime.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-3584