Vulnerability Spotlight: CVE-2023-32692
Analysis of a Remote Code Execution vulnerability in the PHP Framework CodeIgniter
During a recent penetration test, we were looking at an older web application built with the PHP Framework CodeIgniter. To better understand the framework’s attack surface, I reviewed its published security advisories. One entry stood out immediately: CVE-2023-32692, a remote code execution vulnerability, that based on the advisory seems a logical bug within the frameworks validation feature. This post walks through the vulnerability analysis and basic exploitation.
Vulnerability Description
The issue affects CodeIgniter versions prior to 4.3.5. The following quote is from the official CodeIgniter security advisory:
This vulnerability allows attackers to execute arbitrary code when you use Validation Placeholders.
The vulnerability exists in the Validation library, and validation methods in the controller and in-model validation are also vulnerable because they use the Validation library internally.
The advisory references the CodeIgniter documentation for Validation Placeholders, which itself includes a telling warning added in the fixed version:
Since v4.3.5, you must set the validation rules for the placeholder field (the id field in the sample code above) for security reasons. Because attackers can send any data to your application.
Setting Up a Test Environment
When doing vulnerability analysis, I always prefer an environment that allows me to use a debugger. For PHP-based applications, my go-to solution is Visual Studio Code with a PHP-based dev container (an underrated feature that keeps your local VS Code instance clean by running all extensions and tooling inside a containerized Linux system). You can find the complete setup on our GitHub page; only VS Code and Docker are required.
The Dockerfile is based on the official PHP Docker image, using PHP 8.3 since the vulnerable CodeIgniter version does not work with newer PHP releases. It also installs XDebug for stepping through code and Composer for dependency management.
The devcontainer includes the following VS Code extensions:
| Extension | Purpose |
|---|---|
| xdebug.php-debug | PHP XDebug support for debugging the application |
| bmewburn.vscode-intelephense-client | PHP language support for VS Code |
| humao.rest-client | Sending HTTP requests directly from the editor |
The full process of starting the example application within a debugging session and configuring breakpoints is documented in the GitHub repository.
Vulnerability Analysis
To understand the vulnerability, let’s first look at how CodeIgniter defines validation rules as strings. From the official documentation:
$validation->setRules([
'email' => 'required|max_length[254]|is_unique[users.email,id,{id}]',
]);
CodeIgniter uses a pipe character (|) to separate validation rules for a field and parameters to rules are passed inside square brackets. The {id} placeholder will be substituted with the value of the id field from the submitted form data before validation runs.
This substitution is where the vulnerability lives. Inside CodeIgniter’s Validation class, the run method calls the following before any validator executes:
126// Replace any placeholders (e.g. {id}) in the rules with
127// the value found in $data, if any.
128$this->rules = $this->fillPlaceholders($this->rules, $data);
The docblock of fillPlaceholders clearly describes the intended behavior:
639/**
640 * Replace any placeholders within the rules with the values that
641 * match the 'key' of any properties being set. For example, if
642 * we had the following $data array:
643 *
644 * [ 'id' => 13 ]
645 *
646 * and the following rule:
647 *
648 * 'required|is_unique[users,email,id,{id}]'
649 *
650 * The value of {id} would be replaced with the actual id in the form data:
651 *
652 * 'required|is_unique[users,email,id,13]'
653 */
The problem is straightforward: user-supplied input is interpolated directly into the rule string without any sanitization. A closing bracket terminates the current rule’s parameter list, and a pipe character introduces a new rule. This is structurally identical to classic injection attacks — the framework trusts user input as part of a structured string, and that trust is abused to alter the intended control flow.
If we send the following parameters in an HTTP request:
id=1]|injected_rule[parameters]|second_rule[&email=foo@acme.com
The resulting validation rule string after fillPlaceholders runs becomes:
required|valid_email|is_unique[users.email,id,1]|injected_rule[parameters]|second_rule[]
The trailing second_rule[ is needed to absorb the dangling ] from the original rule template.

Callable Validation Rules
CodeIgniter provides a large number of general rules, but none are useful from an attacker’s perspective on their own. However, CodeIgniter also supports Callable Rules — arbitrary PHP functions used as validators. The relevant excerpt from the processRules method in CodeIgniter’s Validation class:
281foreach ($rules as $i => $rule) {
282 $isCallable = is_callable($rule);
283
284 $passed = false;
285 $param = false;
286
287 if (! $isCallable && preg_match('/(.*?)\[(.*)\]/', $rule, $match)) {
288 $rule = $match[1];
289 $param = $match[2];
290 }
291
292 // Placeholder for custom errors from the rules.
293 $error = null;
294
295 // If it's a callable, call and get out of here.
296 if ($this->isClosure($rule)) {
297 $passed = $rule($value, $data, $error, $field);
298 } elseif ($isCallable) {
299 $passed = $param === false ? $rule($value) : $rule($value, $param, $data);
300 } else {
301 $found = false;
Since PHP’s is_callable returns true for any callable function, we can inject any PHP function that accepts at least one parameter — including dangerous ones like system or passthru.
By injecting a call to system and using the email field value as the OS command, we get remote code execution:
id=1]|system|[&email=touch+/tmp/pwn
Bypassing Prior Validation Rules
One complicating factor is that the injected callable only runs if all preceding rules in the chain pass. This can restrict payload length or impose format requirements.
Consider the following rule chain, where the developer has added email format validation: ’email’ => ‘required|valid_email|is_unique[users.email,id,{id}]’,
In this case, the value passed to system() must first satisfy valid_email. Looking at the implementation, it is essentially a wrapper around PHP’s filter_var:
public function valid_email(?string $str = null): bool
{
// @see https://regex101.com/r/wlJG1t/1/
if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && preg_match('#\A([^@]+)@(.+)\z#', $str ?? '', $matches)) {
$str = $matches[1] . '@' . idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46);
}
return (bool) filter_var($str, FILTER_VALIDATE_EMAIL);
}
Two techniques combine to bypass this filter. First, bash’s ${IFS} variable — the Internal Field Separator — expands to whitespace, allowing us to replace spaces in OS commands: touch /tmp/pwned becomes touch${IFS}/tmp/pwned. Second, since system() receives the entire email string including the @domain portion, we chain a harmless secondary command using && to absorb the suffix. The result:
touch${IFS}/tmp/pwned&&ls@foobar.com
This string satisfies filter_var’s email check and when passed to system(), creates the file /tmp/pwned.
Remediation
The fix is straightforward: upgrade to CodeIgniter 4.3.5 or later. As noted in the advisory, you must also define explicit validation rules for any field used as a placeholder.
Would AI Have Found This?
Yes. This resembles a medium-difficulty CTF challenge, so identifying the vulnerability from the advisory should not be challenging for an LLM with good reasoning. I tested various models with basic custom harnesses; all of them identified the bug within minutes (based on the released vulnerability advisory).
Conclusion
CVE-2023-32692 is a great example for anyone looking to get started with subtle, non-obvious bugs in PHP-based web applications. The vulnerable feature (Validation Placeholders) is likely not widely used in the wild, though I haven’t done any systematic research on open-source projects built on CodeIgniter to confirm that assumption.
Thanks to Hannes Köttner on Unsplash for the title picture.
