# RCA Report — GHSA-4q58-jw8x-8cm7

## Summary

CodeIgniter4's View Parser evaluates `{if ...}` / `{elseif ...}` conditional
tags by splicing the *unmodified* tag expression into a PHP `if (...)`:
statement and running the resulting template through `eval()`. When an
application renders user-editable template source (e.g. an online template
editor) with `Parser::render()`/`renderString()`, any user who can edit the
template can inject arbitrary PHP expressions into the conditional tag and
achieve server-side arbitrary PHP code execution (CWE-94). Version 4.7.5 adds
an opt-in restriction (`Config\View::$restrictParserConditionals` or the
per-render `restrictConditionals` option) that tokenizes the condition and
rejects anything beyond variables, literals, and comparison/logical operators.

## Impact

- Package/component: `codeigniter4/framework`, `system/View/Parser.php`
  (`Parser::parseConditionals()`)
- Affected versions: `< 4.7.5` (reproduced on v4.7.4, commit
  `2bd0f01d2813f9ec06db42643ce39d9f5428bf6d`); **4.7.5 remains exploitable
  unless the application explicitly enables `restrictParserConditionals` /
  the `restrictConditionals` render option** (confirmed at runtime).
- Risk: High (CVSS 8.8). Remote, low-privilege attacker with template-edit
  capability gains full PHP code execution in the web server process:
  arbitrary command execution, file read/write, data theft, pivoting.

## Impact Parity

- Disclosed/claimed maximum impact: arbitrary PHP code execution (code
  injection, CWE-94) via View Parser conditional tags — `code_execution`.
- Reproduced impact from this run: arbitrary PHP code execution through the
  real HTTP endpoint of a running CodeIgniter 4.7.4 application. The payload
  `{if (file_put_contents(getenv('PRUVA_MARKER_DIR').'/vuln1.txt','...')!==false) && print(shell_exec('id'))}COND_BRANCH_OK{endif}`
  wrote attacker-named marker files to disk *and* returned the output of
  `shell_exec('id')` (`uid=1000(vscode) gid=1000(vscode) ...`) in the HTTP
  response body.
- Parity: **full**. No gap between claimed and demonstrated impact.

## Root Cause

`Parser::parseConditionals()` (v4.7.4, `system/View/Parser.php:455`)
extracts each `{if CONDITION}` tag with a regex and replaces it with
`<?php if (CONDITION): ?>`, embedding the attacker-controlled `CONDITION`
verbatim into PHP source. The whole template is then executed with
`eval('?>' . $template . '<?php ')` after `extract($this->tempData)`. The
only sanitization applied beforehand is a `str_replace` of literal `<?` /
`?>`, which does nothing to stop code injected *through* the conditional
expression itself. Because the condition is arbitrary PHP, expressions such
as `(file_put_contents(...)!==false) && print(shell_exec('id'))` execute
with the privileges of the PHP process.

Fix (v4.7.5): `Parser::parseTemplate()` computes
`$this->restrictConditionals = ... || (bool) ($options['restrictConditionals'] ?? $this->config->restrictParserConditionals)`
and `parseConditionals()` throws `ViewException::forRestrictedConditional()`
when `isRestrictedCondition()` (a `PhpToken::tokenize` allow-list of
variables, literals, arithmetic/comparison/logical operators, and grouping
parentheses) rejects the expression. The restriction is **opt-in**
(`public bool $restrictParserConditionals = false;` default), so upgrading
alone does not close the hole — verified at runtime in this run.

Advisory: https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-4q58-jw8x-8cm7
Fix diff: `git diff v4.7.4 v4.7.5 -- system/View/Parser.php app/Config/View.php`

## Reproduction Steps

1. `bundle/repro/reproduction_steps.sh` (self-contained; run twice
   consecutively, both runs exit 0).
2. The script:
   - installs PHP CLI + composer if missing;
   - checks out CodeIgniter4 `v4.7.4` (vulnerable) into
     `<project_cache_dir>/repo` and `v4.7.5` (fixed) into
     `bundle/artifacts/ci475`, verifying the patch hunk is absent/present;
   - `composer install --no-dev` in both apps;
   - injects a `TemplateRender` controller exposing `POST /render`, which
     passes the request's `template` body straight into
     `service('parser')->setData([...])->renderString($template, $options)`
     (with `restrictConditionals=true` when `restrict=1`);
   - serves both apps over HTTP with the PHP built-in web server (the exact
     command `php spark serve` execs), health-checks `GET /`;
   - sends the conditional-tag payload twice per scenario and asserts:
     - v4.7.4: marker file created with the unique token **and** `uid=` from
       `shell_exec('id')` present in the HTTP response (RCE confirmed);
     - v4.7.5 + `restrict=1`: no marker, no `uid=`, response is a 500
       `ViewException: The Parser conditional is not allowed in restricted mode`;
     - v4.7.5 + `restrict=0` (default): marker created, `uid=` present —
       advisory note "upgrading alone is insufficient" confirmed.
3. Expected evidence: `[+] vuln attempt N: arbitrary PHP executed`, `[+]
   fixed-restricted attempt N: payload neutralized`, `[+] fixed-unrestricted
   attempt N: still exploitable`, final `RESULT: CONFIRMED`, exit code 0.

## Evidence

- Full run log: `bundle/logs/reproduction_steps.log`
- Server logs: `bundle/logs/vuln_server.log`, `bundle/logs/fixed_server.log`
  (PHP built-in server request logs for both apps)
- HTTP request/response captures: `bundle/artifacts/http/` (e.g.
  `vuln1_request.txt` = payload, `vuln1_response.txt` contains
  `uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)` + `COND_BRANCH_OK`;
  `fixedr1_response.txt` contains the `ViewException` restricted-mode message)
- Marker files written by the eval'd payload: `bundle/repro/markers/vuln1.txt`,
  `vuln2.txt`, `fixedu1.txt`, `fixedu2.txt` (each contains the per-attempt
  unique token `GHSA-4q58-jw8x-8cm7 arbitrary PHP executed token=...`)
- Runtime manifest with target identity and SHA-256 of every proof artifact:
  `bundle/repro/runtime_manifest.json`
- Environment: Ubuntu 26.04, PHP 8.5.4 (cli, NTS), Composer 2.9.5,
  CodeIgniter v4.7.4 (`2bd0f01d...`) and v4.7.5 (`36256090...`), served via
  `php -S localhost:8090/8091` with `CI_ENVIRONMENT=production`.

## Recommendations / Next Steps

- Upgrade to `codeigniter4/framework >= 4.7.5` **and** set
  `Config\View::$restrictParserConditionals = true` (or pass
  `['restrictConditionals' => true]` to every `render()`/`renderString()` call
  that processes less-trusted template source). Upgrading without enabling the
  restriction leaves the application exploitable.
- Longer-term, the framework should consider making the restricted mode the
  default for `renderString()` of non-file templates.
- Test recommendation: regression test asserting that a conditional tag
  containing a function call raises `ViewException::forRestrictedConditional`
  when restriction is enabled (this exact behavior was observed at runtime).

## Additional Notes

- Idempotency: the script is fully re-runnable. It reuses the prepared
  project cache checkout (`/pruva/project-cache/repo`) and composer `vendor/`
  when present, kills stale `php -S` listeners on ports 8090/8091 before
  starting fresh servers, cleans the marker directory, and regenerates all
  HTTP captures, markers, and `runtime_manifest.json` with fresh per-attempt
  tokens on every run. Two consecutive clean runs passed (exit 0).
- The reproduction uses the framework repository's own application skeleton
  (`app/`, `public/`, `system/rewrite.php`) served in `production` mode — no
  mocks, no sanitizer, no reimplementation of the vulnerable code.
- Edge case noted: `spark serve` silently increments the port when the
  requested one is busy; the script therefore runs the underlying `php -S`
  command directly and pre-kills stale listeners so the healthcheck and
  exploit always target the intended instance.
