Skip to content

CVE-2026-63223: Verified Reproduction

CVE-2026-63223: CodeIgniter4 is image/mime in upload validation bypass — unrestricted file upload leading to RCE

CVE-2026-63223 is verified against codeigniter4/framework · github. Affected versions: < 4.7.4. Fixed in 4.7.4. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00320.

REPRO-2026-00320 codeigniter4/framework · github RCE Aug 23, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
24m 2s
Tool calls
145
Spend
$3.02
01 · Overview

What Is CVE-2026-63223?

CVE-2026-63223 is a critical-severity RCE vulnerability affecting codeigniter4/framework < 4.7.4. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00320).

02 · Severity & CVSS

CVE-2026-63223 Severity

CVE-2026-63223 is rated critical severity.

CRITICAL threat level
Weakness CWE-434 — Unrestricted Upload of File with Dangerous Type

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected codeigniter4/framework Versions

codeigniter4/framework · github versions < 4.7.4 are affected.

How to Reproduce CVE-2026-63223

$ pruva-verify REPRO-2026-00320
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00320/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-63223

Remote code execution — reproduced
  • 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
Trigger

multipart upload of a GIF89a+PHP polyglot with client filename shell.php to POST /upload validated by the is_image rule (no ext_in)

Attack chain
  1. POST /upload (CodeIgniter4 Upload controller, StrictRules FileRules is_image)
  2. saved as public/uploads/shell.php
  3. GET /uploads/shell.php?cmd=... executes attacker commands
Runnable proof: reproduction_steps.sh
Captured evidence: service fixed 1service fixed 2
How the agent worked 308 events · 145 tool calls · 24 min
24 minDuration
145Tool calls
61Reasoning steps
308Events
10Dead-ends
Agent activity over 24 min
Policy
1
Support
11
Repro
100
Judge
57
Variant
134
Verify
1
0:0023:50

Root Cause and Exploit Chain for CVE-2026-63223

Versions: < 4.7.4 (reproduced on v4.7.3).

CodeIgniter4 versions before 4.7.4 validate file uploads with the is_image() and mime_in() rules (system/Validation/StrictRules/FileRules.php) using only the content-sniffed MIME type (finfo magic bytes). They never compare the client-supplied filename extension with the detected content type. An attacker can therefore upload a polyglot file whose first bytes are GIF89a (sniffed as image/gif) but whose body contains PHP code, named shell.php. Validation trusts the content (image ✔); the web server trusts the extension (.php → execute). This confused-deputy gap yields unrestricted file upload and remote code execution when the application saves the upload under the client filename into a web-accessible, PHP-enabled directory.

  • Package/component: codeigniter4/frameworkCodeIgniter\Validation\StrictRules\FileRules::is_image() and ::mime_in() (non-strict FileRules is an empty subclass, so both rule sets are affected).
  • Affected versions: < 4.7.4 (reproduced on v4.7.3).
  • Risk level: critical — unauthenticated remote code execution on any application that (1) validates uploads with is_image or mime_in without ext_in, (2) preserves the client filename on save, and (3) stores uploads in a web-accessible, PHP-enabled directory.

Impact Parity

  • Disclosed/claimed maximum impact: code execution (unrestricted file upload → RCE).
  • Reproduced impact from this run: code execution — a GIF89a+PHP polyglot uploaded as shell.php passed is_image validation on v4.7.3, was saved to public/uploads/shell.php, and an HTTP GET to /uploads/shell.php?cmd=... executed attacker-controlled commands (echo <unique marker> and id, returning uid=1000(vscode) ...) in 2/2 clean attempts.
  • Parity: full.
  • Not demonstrated: nothing — the claimed impact was reproduced end-to-end through the real HTTP boundary.

Root Cause

FileRules::is_image() (v4.7.3) checks uploaded[], then calls $file->getMimeType() (finfo content sniffing) and accepts the file if the detected type starts with image. mime_in() likewise compares only the sniffed type against an allow-list. Neither rule inspects $file->getClientExtension() / $file->getClientName(). Because a polyglot can simultaneously be valid GIF89a content and executable PHP source, the validator and the web server reach different conclusions about the same file:

  • Fix (v4.7.4, release commit 67ead895b7491703e5e5bc17436778806192008f): adds hasInvalidImageClientExtension() to is_image() (reject non-empty client extensions that do not map to an image/* MIME type) and hasMismatchedClientExtension() to mime_in() (reject client extensions that do not match the extension guessed from the sniffed content), in system/Validation/StrictRules/FileRules.php.
  • Note: the ticket names fixed commit b6e9a4fa. That hash is not present in the codeigniter4/framework distributable mirror (which receives squashed release commits from the codeigniter4/CodeIgniter4 development repo). The v4.7.4 release tag was verified to contain exactly the named fix helpers and was used as the fixed checkout; patch-anchor verification (helper absent in v4.7.3, present in v4.7.4) is enforced by the script on every run.

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; run twice consecutively, exit 0 both times).
  2. The script:
    • Installs PHP CLI + extensions and Composer if absent.
    • Clones codeigniter4/framework into the prepared project cache (<project_cache_dir>/repo) or bundle/artifacts/framework as fallback.
    • Verifies the patch anchor (fix helper absent at v4.7.3 / present at the fixed ref).
    • Writes a real upload controller (app/Controllers/Upload.php) using the rule uploaded[userfile]|is_image[userfile] (no ext_in) that saves with the client filename into public/uploads/, plus a POST /upload route.
    • Builds a GIF89a+PHP polyglot (verified to sniff as image/gif) and a plain-text negative control.
    • Starts the real product server (php spark serve, PHP built-in web server bound to 127.0.0.1) and runs, for both v4.7.3 and the fixed ref:
      • Negative control: plain-text plain.php upload → rejected (rule active).
      • 2 clean vulnerable attempts: POST polyglot as shell.php, then GET /uploads/shell.php?cmd=echo <unique marker>;id.
      • 2 clean fixed attempts: same POST, then GET.
  3. Expected evidence: v4.7.3 returns {"status":"saved"} and the GET response contains the unique per-attempt marker plus uid= output (and not the raw <?php source); the fixed build returns HTTP 400 {"status":"rejected"}, writes no file, and the GET returns 404.

Evidence

  • Main log: bundle/logs/reproduction_steps.log.
  • Per-attempt artifacts (listed in bundle/repro/runtime_manifest.json):
    • bundle/logs/repro/vuln_attempt{1,2}_upload.json{"status":"saved","path":"uploads/shell.php"}
    • bundle/logs/repro/vuln_attempt{1,2}_shell_get.txt → e.g.
      GIF89a;
      RCE_1_1785590766_8732
      uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)
      
    • bundle/logs/repro/fixed_attempt{1,2}_upload.json{"status":"rejected","errors":{"userfile":"userfile is not a valid, uploaded image file."}} (HTTP 400), shell GET HTTP 404.
    • bundle/logs/repro/control_upload.json → plain-text upload rejected on the vulnerable build (proves is_image is enforced, not bypassed by harness misconfiguration).
    • bundle/logs/repro/service_{vuln,fixed,control}_*.logphp spark serve product server logs.
  • Environment: Ubuntu 26.04, PHP 8.5.4 (cli, distro package), Composer 2.10.2, CodeIgniter v4.7.3 (ab9bf33) vulnerable vs v4.7.4 (67ead89) fixed; server = product CLI php spark serve (PHP built-in web server), which executes .php files under public/ exactly as a standard Apache/FPM deployment would for a PHP-enabled upload directory.

Recommendations / Next Steps

  • Upgrade to CodeIgniter ≥ 4.7.4, which rejects uploads whose client filename extension is inconsistent with the sniffed content type.
  • Defense in depth for applications regardless of framework version: always add ext_in (and max_size) to upload rules, never preserve client filenames ($file->getRandomName()), store uploads outside the webroot or behind a controller, and disable PHP execution in upload directories at the web-server layer.
  • Testing: add regression tests that upload a GIF89a+PHP polyglot named shell.php and assert rejection under is_image/mime_in.

Additional Notes

  • Idempotency: the script was run twice consecutively; both runs exited 0 with 2/2 vulnerable RCE attempts and 2/2 fixed rejections. It re-checkouts, re-applies the overlay, and removes public/uploads before every attempt, so it is safe to re-run.
  • Edge cases/limitations: RCE requires the three deployment preconditions listed above (validation without ext_in, client filename preserved, web-accessible PHP-enabled upload dir). The proof uses php spark serve (the framework's documented development server built on PHP's web server); any deployment that executes .php under the upload directory (Apache mod_php, PHP-FPM with typical location rules) exhibits the same behavior. If uploads are stored outside the docroot or PHP is disabled there, the validation bypass still occurs (file is accepted) but code execution is not reachable — that is a deployment mitigation, not a fix.
  • The named fixed commit b6e9a4fa could not be resolved in the codeigniter4/framework mirror; the v4.7.4 tag contains the exact helpers described in the advisory and was used with patch-anchor verification.

CVE-2026-63223 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.

Event 1/40
0:002:59
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-63223 · REPRO-20
0:04
0:06
web search
0:11
0:16
0:49
0:49
extract_facts
no facts extracted
0:51
0:51
supportclaim_contract
1:00
1:00
1:00
1:04
1:04
1:04
1:08
1:09
$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
0.4s
total 4
drwx------ 4 vscode vscode   80 Aug  1 13:17 .
drwxr-xr-x 3 root   root   4096 Aug  1 13:18 ..
drwx------ 2 vscode vscode   60 Aug  1 13:17 .pruva
drwx------ 2 vscode vscode   40 Aug  1 13:17 repo-mirrors
{
  "schema_version": 1,
  "entries": [
1:09
1:27
1:28
$php -v 2>&1 | head -3; composer --version 2>&1 | head -1; which curl git jq; php -m 2>/dev/null | grep -Ei 'fileinfo|mbstring|intl|curl|xml|json' | head
0.4s
pruva-command: 1: php: not found
pruva-command: 1: composer: not found
/usr/bin/curl
/usr/local/bin/git
/usr/bin/jq
1:51
1:52
$id; which sudo apt-get apt 2>&1; cat /etc/os-release | head -3
0.4s
uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)
/usr/bin/sudo
/usr/bin/apt-get
/usr/bin/apt
PRETTY_NAME="Ubuntu 26.04 LTS"
NAME="Ubuntu"
VERSION_ID="26.04"
2:44
$sudo apt-get update -qq 2>&1 | tail -2 && sudo apt-get install -y -qq php-cli php-xml php-mbstring php-intl php-curl php-sqlite3 unzip 2>&1 | tail -3; php -v | head -2
35.1s
sysctl: setting key "kernel.yama.ptrace_scope", ignoring: Read-only file system
sysctl: setting key "vm.mmap_min_addr", ignoring: Read-only file system
Processing triggers for php8.5-cli (8.5.4-0ubuntu1.2) ...
PHP 8.5.4 (cli) (built: Jul 16 2026 18:56:38) (NTS)
Copyright (c) The PHP Group
2:55
$cd /tmp && curl -sS https://getcomposer.org/installer -o composer-setup.php && php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer 2>&1 || sudo php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer; composer --version 2>&1 | head -1
1.1s
The installation directory "/usr/local/bin" is not writable
Composer version 2.10.2 2026-07-01 11:24:45
2:58
2:59
$cd /pruva/project-cache && git clone --quiet https://github.com/codeigniter4/framework.git repo 2>&1 | tail -1; cd repo && git cat-file -t b6e9a4fa 2>&1; git tag -l 'v4.7.*' | tail -5; git log --oneline -1 b6e9a4fa 2>&1
1.4s
{"cwd":"/workspace","duration_ms":1037,"exit_code":128,"stderr":"","stdout":"fatal: Not a valid object name b6e9a4fa\nv4.7.0\nv4.7.1\nv4.7.2\nv4.7.3\nv4.7.4\nfatal: ambiguous argument 'b6e9a4fa': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n","success":false,"timeout_secs":600}
2:59
error

Unknown error

08 · How to Fix

How to Fix CVE-2026-63223

Upgrade codeigniter4/framework · github to 4.7.4 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-63223 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-63223

Is CVE-2026-63223 exploitable?

Yes. Pruva independently reproduced CVE-2026-63223 in codeigniter4/framework and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00320).

How severe is CVE-2026-63223?

CVE-2026-63223 is rated critical severity.

What type of vulnerability is CVE-2026-63223?

CVE-2026-63223 is classified as CWE-434 (Unrestricted Upload of File with Dangerous Type), a RCE vulnerability.

Which versions of codeigniter4/framework are affected by CVE-2026-63223?

codeigniter4/framework < 4.7.4 is affected by CVE-2026-63223.

Is there a fix for CVE-2026-63223?

Yes. CVE-2026-63223 is fixed in codeigniter4/framework 4.7.4. Upgrading to the fixed version remediates the issue.

How can I reproduce CVE-2026-63223?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-63223 reproduction verified?

Yes. Pruva reproduced CVE-2026-63223 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-63223

Authoritative sources for CVE-2026-63223 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.