A bouncer who only checks your ID photo but never reads the name — anyone with a stolen headshot walks right in
CVE-2026-63223 is an unrestricted file upload flaw in CodeIgniter 4's is_image and mime_in validation rules. These rules inspect only the content-derived MIME type (magic bytes like GIF89a or \x89PNG) but never verify the client-supplied filename extension. An attacker prepends image magic bytes to a PHP webshell, names it shell.php, and the validator waves it through. All CodeIgniter 4 versions from 4.0.0-alpha.3 through 4.7.3 are affected; the fix ships in v4.7.4. The attack is unauthenticated when the upload endpoint is publicly accessible.
The vendor's CVSS 9.8 CRITICAL reflects the *worst-case outcome* — unauthenticated RCE — and the CVSS vector is technically correct for that scenario. However, the 9.8 silently assumes three application-level conditions all hold: the app uses is_image/mime_in *without* an independent ext_in check, the app saves files using the original client filename, and uploads land in a web-accessible directory where PHP execution is enabled. Each condition is individually common in tutorial-grade CI4 code, but the simultaneous AND of all three plus a publicly-reachable upload endpoint narrows the truly-exploitable population substantially. The CVSS score does not model this conditional chain, making the vendor label an overstatement for fleet-wide prioritization.
5 steps from start to impact.
Identify a CodeIgniter 4 upload endpoint
/writable/ path leaks) or Wappalyzer-style technology detection can reveal the framework. The upload endpoint must accept file submissions — profile pictures, document uploads, avatar changes, etc.- Target runs CodeIgniter 4 < 4.7.4
- A file upload endpoint is reachable from the attacker's network position
- Not all CI4 apps expose upload functionality
- Many upload endpoints require authentication, reducing the unauthenticated attack surface
Craft a polyglot PHP-image payload
GIF89a, \xFF\xD8\xFF\xE0, or \x89PNG\r\n\x1A\n) to a PHP webshell payload (e.g., <?php system($_GET['cmd']); ?>). The resulting file has a .php extension but begins with valid image headers. The imbas007/CVE-2026-63223-POC Python script automates this step, generating the polyglot and uploading it in one command.- Knowledge of the magic bytes trick (widely documented, automated by public PoC)
- Essentially zero — the technique is decades old and fully automated by the published PoC
GIF89a followed by <?php catch this pattern.Upload passes is_image / mime_in validation
finfo_file() or equivalent, which reads the magic bytes and returns an image MIME type. The is_image rule confirms it is a recognized image type; mime_in confirms the MIME is in the allowed list. Neither rule inspects the .php extension. The file is accepted.- Application uses
is_imageormime_inwithout a companionext_inrule - Application preserves the client-supplied filename (does not call
getRandomName())
- Apps that add
ext_in[jpg,png,gif]alongsideis_imageblock this step entirely - Apps using
getRandomName()strip the.phpextension, neutralizing the payload - Security-conscious CI4 configurations are not uncommon — the official docs mention
ext_in
ext_in in upload validation chains. SAST scanners detect move() with client filename.File stored in web-accessible directory with PHP execution
/public/uploads/) where the web server (Apache, Nginx) is configured to execute .php files. The attacker now has a webshell at a predictable URL path.- Uploads stored under web root
- PHP execution not disabled for the upload directory
- Best practice is to store uploads outside web root or in cloud object storage (S3, GCS)
- Adding
php_flag engine off(.htaccess) orlocation ~ \.php$ { deny all; }(Nginx) in the upload directory blocks execution - Containerized deployments with read-only filesystem layers prevent writes to web root
.php files in upload directories. EDR detects PHP process spawning shell commands.Webshell execution — full RCE
GET /uploads/shell.php?cmd=id). The web server parses it as PHP, executes the embedded code, and returns command output. From here the attacker has code execution as the web server user (typically www-data or apache), with access to database credentials in .env, session storage, and potential lateral movement vectors.- Web server processes
.phpfiles in the upload directory - No application-level access control on the uploads path
- SELinux/AppArmor policies may confine the web server process
- Cloud-native deployments on read-only containers limit post-exploitation pivoting
sh, whoami, curl). SIEM rules for webshell indicators (encoded eval, base64 decode in access logs). GreyNoise and honeypot networks would detect scanning for uploaded shells.ext_in[jpg,png,gif,webp] to all upload validation rule sets — The root cause is missing extension validation. Adding ext_in alongside is_image/mime_in rejects any file whose client-supplied extension is not in the safe list, neutralizing the polyglot attack entirely. This is a one-line code change per upload handler. Deploy within 30 days per the noisgate mitigation SLA for HIGH.$file->getRandomName() — Even if a .php payload passes validation, renaming it to a random string with a safe extension (e.g., 1696012345_abc123.jpg) strips the executable extension. The webshell cannot be triggered. Deploy within 30 days.php_flag engine off in .htaccess (Apache) or location ~ \.php$ { deny all; } (Nginx) for the uploads path. Even if a .php file lands in the directory, the web server returns it as plaintext or 403. This is an infrastructure-level control independent of application code. Deploy within 30 days./var/uploads/) or cloud object storage eliminates the ability to request the file via HTTP and trigger PHP execution. Serve uploads through an application-controlled download endpoint instead. Deploy within 30 days.<?php or <?= inside multipart file upload bodies. This provides defense-in-depth but is bypassable with encoding tricks (short tags, base64 eval), so treat as a supplementary layer only. Deploy within 30 days.- CSP headers — Content-Security-Policy governs browser-side script execution, not server-side PHP interpretation. A webshell executes on the server; CSP is irrelevant.
- Rate limiting on the upload endpoint — Slows brute-force but doesn't prevent a single successful upload. The attacker needs exactly one request.
- Antivirus scanning of uploads — Most AV engines detect known webshell signatures but the polyglot technique with custom payloads easily evades signature-based scanning. Not a reliable control.
- MIME type validation alone (the vulnerable control itself) — Adding more MIME checks without extension validation is the exact flaw this CVE describes. Do not rely on
mime_inas a fix foris_imageor vice versa.
The supporting signals.
| In-the-Wild Exploitation | No confirmed in-the-wild exploitation. Not listed on CISA KEV. No campaigns attributed. GreyNoise shows no tagged scanning activity for this CVE as of 2026-10-07. |
|---|---|
| Proof-of-Concept | Public, weaponized. imbas007/CVE-2026-63223-POC — Python script automating payload generation, upload, and interactive shell. Also referenced in the asvorg/CVE-2026-PoCs and SecureWithUmer/CVE-2026-PoCs community collections. A second PoC by shinthink was noted in search results. No Nuclei template confirmed yet. |
| EPSS | 0.008 (0.8%) — well below the 90th percentile threshold (~0.10). Indicates low predicted exploitation probability in the next 30 days despite the CRITICAL CVSS label. |
| CISA KEV Status | Not listed as of 2026-10-07. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — Network-reachable, no authentication, no user interaction, full CIA impact. The vector is technically accurate for the worst-case scenario but does not model the three application-level prerequisites. |
| Affected Versions | CodeIgniter 4 from 4.0.0-alpha.3 through 4.7.3 inclusive. All minor branches (4.0.x–4.7.x) are affected. |
| Fixed Version | CodeIgniter 4.7.4 (patch commit b6e9a4f). Adds cross-validation of getClientExtension() against guessExtension() in upload rules. |
| Exposure / Installed Base | Approximately 40,000–70,000 websites running CodeIgniter (Datanyze, Wappalyzer, WebTechSurvey). ~0.76% market share in web frameworks (Enlyft). Top markets: India (23%), United States, Brazil. Named users include Bank Rakyat Indonesia, Supreme Court of India. |
| Disclosure Date | 2026-07-31 (GHSA-mmj4-63m4-r6h5 published simultaneously with the v4.7.4 fix release). |
| Reporter | Not publicly attributed in the GHSA. PoC published by imbas007 (GitHub). |
Sources.
Why this verdict
- Conditional chain narrows exploitable population: The CVSS 9.8 assumes a clean path to RCE, but three application-level conditions must *all* hold:
is_image/mime_inused withoutext_in, client filename preserved, and uploads in a PHP-executable web directory. Each condition independently filters out a material share of CI4 installs. The AND of all three plus an unauthenticated upload endpoint plausibly covers ~5–15% of the CI4 installed base. This compounding friction justifies pulling below CRITICAL. - PoC availability holds severity above MEDIUM: A fully weaponized Python exploit (imbas007) exists with single-target and mass-scan modes. The magic-bytes technique is trivial and decades old. Any attacker who finds a qualifying upload endpoint can exploit it with zero skill. This keeps the floor at HIGH.
- EPSS corroborates limited real-world traction: At 0.008 (0.8%), EPSS is well below the exploitation-activity threshold. No KEV listing, no GreyNoise tags, no attributed campaigns. The gap between the scary CVSS label and actual exploitation evidence is substantial.
- Role multiplier: CodeIgniter 4 is a general-purpose PHP web framework — it is *not* a canonical high-value infrastructure component (not an IdP, hypervisor, PAM, backup, DC role, or network edge appliance). (a) *Low-value role:* dev sandboxes, hobby sites — many of the ~40–70k installs. RCE impact is negligible. (b) *Typical role:* line-of-business web apps, internal portals, CMS. RCE yields
www-datashell + database credentials — host-level blast radius. (c) *High-value role:* banking portals (Bank Rakyat Indonesia), government apps (Supreme Court of India). These represent <5% of CI4's installed base. When the chain succeeds here, blast radius is data-tier (customer PII, financial records) but still host-scoped — not domain/fleet/supply-chain scale. No canonical ≥10% high-value-role floor applies. The floor is HIGH based on weaponized PoC + unauthenticated RCE when conditions hold.
Why not higher?
Elevating to CRITICAL would require that the exploit chain succeed across the majority of CI4 installs without additional application-level conditions. In practice, three non-trivial preconditions must simultaneously hold, and the installed base is modest (~40–70k sites, <1% of PHP framework market). EPSS at 0.8% and zero KEV/in-the-wild signal confirm the theoretical CVSS 9.8 is not translating to real-world CRITICAL urgency.
Why not lower?
Dropping to MEDIUM would underweight the fact that each individual precondition is common in tutorial-following CI4 projects, the exploit is fully automated and unauthenticated, and the end state is unrestricted RCE — not information disclosure or DoS. A weaponized unauthenticated RCE PoC with a plausible 5–15% hit rate on the installed base merits HIGH.
Crowdsourced verification payload.
Run this script on each host running a CodeIgniter 4 application. It checks the installed framework version via Composer's installed.json. Execute as any user with read access to the application directory: bash check_cve_2026_63223.sh /var/www/myapp
#!/usr/bin/env bash
# CVE-2026-63223 Checker — CodeIgniter 4 is_image/mime_in upload bypass
# Usage: bash check_cve_2026_63223.sh /path/to/ci4/project
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
APP_DIR="${1:-}"
if [[ -z "$APP_DIR" ]]; then
echo "Usage: $0 /path/to/ci4/project"
exit 2
fi
# Locate composer installed.json
INSTALLED="$APP_DIR/vendor/composer/installed.json"
if [[ ! -f "$INSTALLED" ]]; then
echo "UNKNOWN — Could not find $INSTALLED. Is this a Composer-managed CI4 project?"
exit 2
fi
# Extract codeigniter4/framework version
VERSION=$(python3 -c "
import json, sys
with open('$INSTALLED') as f:
data = json.load(f)
packages = data.get('packages', data) if isinstance(data, dict) else data
for pkg in packages:
name = pkg.get('name', '')
if name == 'codeigniter4/framework':
v = pkg.get('version', pkg.get('version_normalized', ''))
print(v.lstrip('v'))
sys.exit(0)
print('NOT_FOUND')
" 2>/dev/null)
if [[ "$VERSION" == "NOT_FOUND" || -z "$VERSION" ]]; then
echo "UNKNOWN — codeigniter4/framework not found in $INSTALLED"
exit 2
fi
echo "Detected CodeIgniter4 version: $VERSION"
# Compare version — vulnerable if < 4.7.4
REQUIRED="4.7.4"
if python3 -c "
from packaging.version import Version
import sys
try:
if Version('$VERSION') >= Version('$REQUIRED'):
sys.exit(0)
else:
sys.exit(1)
except Exception:
sys.exit(2)
" 2>/dev/null; then
echo "PATCHED — CodeIgniter4 $VERSION >= $REQUIRED (CVE-2026-63223 fixed)"
exit 0
else
RC=$?
if [[ $RC -eq 1 ]]; then
echo "VULNERABLE — CodeIgniter4 $VERSION < $REQUIRED (CVE-2026-63223 applies)"
# Bonus: check if ext_in is used alongside is_image/mime_in in app code
if grep -rq 'ext_in' "$APP_DIR/app/" 2>/dev/null; then
echo " NOTE: ext_in rule found in application code — may be partially mitigated"
else
echo " WARNING: No ext_in rule found — application likely exploitable if uploads are present"
fi
exit 1
else
echo "UNKNOWN — Could not parse version '$VERSION'"
exit 2
fi
fi