CodeIgniter's bouncer checks your ID photo but never reads the name on the card
CVE-2026-63223 affects CodeIgniter 4 prior to version 4.7.4. The framework's is_image and mime_in upload validation rules classify a file solely by its content-derived MIME type (e.g., inspecting magic bytes). An attacker can prepend valid image magic bytes (GIF89a, PNG header, JFIF) to a PHP webshell, name it shell.php, and pass both is_image and mime_in validation. If the application then saves the file using the client-supplied filename via $file->move($path) into a web-accessible, script-enabled directory, the attacker requests /uploads/shell.php and achieves unauthenticated remote code execution. All CodeIgniter 4.x versions before 4.7.4 are affected.
The vendor assigned CVSS 9.8 CRITICAL, which models the theoretical worst case — unauthenticated, network-accessible, low-complexity. That score is defensible as a ceiling but overstates the median real-world deployment. Exploitation requires a specific compound of application-level coding patterns: the app must accept uploads, validate *only* with is_image/mime_in (no independent extension check), preserve the original client filename on disk, store into a web-accessible directory, and allow PHP execution from that directory. Each condition narrows the exploitable population. Many CodeIgniter applications already use $file->getRandomName(), store uploads outside the webroot, or gate uploads behind authentication. The vendor's 9.8 treats all of these as absent; real deployments rarely line up that perfectly.
5 steps from start to impact.
Identify upload endpoint
- Target runs CodeIgniter 4 < 4.7.4
- Application exposes a file upload endpoint reachable by the attacker
- Many upload endpoints require authentication (PR:N assumption fails)
- Upload features are not universal — many CI apps are CRUD/API-only with no file upload
Craft polyglot payload
shell.php whose first bytes are valid image magic bytes (e.g., GIF89a followed by <?php system($_GET['cmd']); ?>). This polyglot passes is_image (which checks MIME via finfo) and mime_in (which checks the content-derived type matches an allowed list) while retaining the .php extension in the filename.- Knowledge of allowed MIME types configured in the app's validation rules
- Trivial to construct — no real friction here
- Public PoC patterns for GIF89a+PHP polyglots are widely documented
<?php, <?=) can catch naive payloads. Yara rules for polyglot detection exist.Upload and bypass validation
is_image rule calls getimagesize() or finfo_file(), both of which read magic bytes and return a valid image MIME type. The mime_in rule confirms the MIME matches the allowed list. Neither rule inspects the client-supplied filename extension. The upload is accepted.- Application validates with is_image or mime_in only, without an independent ext_in or manual extension check
- Applications that also apply ext_in validation (even the pre-4.7.3 version) add a partial barrier
- Custom validation callbacks checking extension would block this
.php extensions in upload directories.File lands with .php extension in webroot
$file->move('/var/www/html/uploads/')) without renaming. The file is now on disk as /var/www/html/uploads/shell.php and is directly accessible via HTTP.- Application preserves original client filename on disk
- Upload directory is under the web root
- PHP execution is enabled in the upload directory (no .htaccess / nginx location block denying PHP)
- CodeIgniter docs recommend
$file->getRandomName()— many apps follow this guidance - Security-conscious deployments store uploads outside webroot or on object storage (S3)
- Many Apache/nginx configs disable PHP execution in upload directories via
php_flag engine offorlocation ~ /uploads/ { deny all; }
.php files. EDR agents monitoring php-fpm/apache2 child processes.Trigger webshell for RCE
https://target.com/uploads/shell.php?cmd=id via a browser or curl. The web server processes the file as PHP, executing the embedded code with the privileges of the web server user (typically www-data or apache). From here, the attacker has command execution on the host.- Web server processes .php files in the upload directory
- PHP-FPM pool restrictions, open_basedir, disable_functions can limit post-exploitation
- SELinux/AppArmor policies on the web server process can block lateral movement
The supporting signals.
| In-the-Wild Exploitation | No confirmed in-the-wild exploitation as of 2026-07-31. Not listed on CISA KEV. |
|---|---|
| Proof of Concept | No dedicated public PoC repository identified, but the technique is trivial — GIF89a+PHP polyglot uploads are a textbook CWE-434 pattern. The GitHub issue #3184 documents the is_image validation weakness. |
| EPSS Score | Not yet scored (CVE published 2026-07-31, EPSS typically lags 24-48h). Expect moderate EPSS given framework-level conditions required. |
| KEV Status | Not listed on CISA KEV as of 2026-07-31. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8). Network vector, no auth, no interaction. The vector does not capture application-level preconditions. |
| Affected Versions | CodeIgniter 4.x < 4.7.4 (all 4.x releases). CodeIgniter 3.x is a separate codebase and is not affected by this specific advisory. |
| Fixed Version | 4.7.4 (released 2026-07-31). Also addresses CVE-2026-48062 (ext_in bypass) and GHSA-hhmc-q9hp-r662 (path traversal in UploadedFile::move()). |
| Related CVE | CVE-2026-48062 — ext_in rule checked MIME-derived extension instead of client filename extension. Fixed in 4.7.3. |
| Exposure Data | CodeIgniter powers ~70K-128K websites globally per Enlyft and BuiltWith. Subset running CI4 (vs legacy CI3) is smaller. No Shodan/GreyNoise scanning campaigns identified yet. |
| Disclosure | Published 2026-07-31 via GHSA-mmj4-63m4-r6h5. Coordinated disclosure with patch available same day. |
noisgate verdict.
The single most decisive factor driving the downgrade from CRITICAL to HIGH is the compound application-level preconditions: exploitation requires the target app to preserve client filenames, store in a script-enabled webroot directory, and lack independent extension validation — a conjunction that excludes the majority of CodeIgniter deployments. The vulnerability is real and weaponizable against misconfigured apps, but the 9.8 models a worst case that most real deployments do not present.
Why this verdict
- Compound preconditions narrow the exploitable population. The chain requires four independent application-level choices to align: upload endpoint exists, no extension check, client filename preserved, PHP execution enabled in upload directory. Each condition roughly halves the eligible target set, compounding to a small fraction of CI4 deployments.
- No authentication requirement is assumed, not guaranteed. The CVSS vector sets PR:N, but many real upload endpoints sit behind login walls, session checks, or CSRF tokens. The vector overstates the unauthenticated attack surface.
- Role multiplier: CodeIgniter is a general-purpose web application framework. (a) *Low-value role:* dev/staging instances — minimal blast radius. (b) *Typical role:* line-of-business web apps — compromise yields one application's data and host-level access as
www-data. (c) *High-value role:* a CI4 app serving as an internal admin panel or customer portal with access to databases holding PII/financial data. In this case the blast radius is tenant-to-host, potentially reaching database credentials and lateral movement. However, CI4 is NOT canonically a high-value-role component (it is not a DC, hypervisor, IdP, PAM, backup server, or network edge appliance). The fraction of CI4 installs in high-value roles is modest (<10%). The floor is therefore HIGH, not CRITICAL. - Polyglot payloads are trivial to construct. GIF89a+PHP webshells are commodity knowledge. No exploit development skill is required, lowering the attacker-capability bar significantly.
- Patch is same-day. The fix (4.7.4) shipped concurrently with disclosure, giving defenders a clear remediation path and reducing the window of exposure for teams that act quickly.
Why not higher?
CRITICAL would require either active exploitation (KEV/campaign evidence), a canonical high-value-role component, or a condition-free attack path. None apply here. The vulnerability requires multiple application-coding decisions to align, and CodeIgniter is a general-purpose web framework — not an identity provider, hypervisor, or network edge device. The exploitable subset of the installed base is meaningfully smaller than the total.
Why not lower?
MEDIUM would understate the risk because when the preconditions DO align, the outcome is unauthenticated RCE — the most severe impact class. The polyglot technique is trivial, no exploit development is needed, and the pattern (upload + execute) is one of the most commonly targeted by automated scanners and botnets. A single vulnerable instance discovered by a scanner leads directly to host compromise with no further interaction.
What to do — in priority order.
- Store uploads outside the web root or on object storage — Move upload targets to a non-web-accessible path (e.g.,
/var/lib/app/uploads/) or use S3/GCS with signed URLs. This completely breaks step 5 of the attack chain. Deploy within 30 days per the noisgate mitigation SLA for HIGH severity. - Randomize filenames on save — Replace
$file->move($path)with$file->move($path, $file->getRandomName())or use$file->store(). This strips the.phpextension and prevents direct execution even if stored in the webroot. A code-level change deployable in a hotfix. - Disable script execution in upload directories — Add
php_flag engine offin.htaccess(Apache) or alocationblock returning 403 for.phpin nginx upload paths. This is a server-config change that requires no application code modification. - Add explicit extension validation — Append
ext_in[field,jpg,png,gif,webp]to your validation rules alongsideis_image/mime_in. On 4.7.4+ this checks the client filename extension. On older versions, combine with a manual check:pathinfo($file->getClientName(), PATHINFO_EXTENSION). - Deploy WAF rules blocking PHP in upload payloads — Configure ModSecurity, Cloudflare WAF, or AWS WAF to inspect multipart upload bodies for PHP opening tags (
<?php,<?=,<?). This catches naive polyglots but can be bypassed with encoding.
- MIME-type validation alone — this IS the vulnerability.
is_imageandmime_inboth check content-derived MIME types, which the attacker controls by prepending magic bytes. - File size limits — a PHP webshell can be as small as 30 bytes. Size restrictions do not mitigate this attack.
- CSRF tokens — CSRF protection prevents cross-site request forgery but does not stop a direct attacker uploading to the endpoint with a valid session or when the endpoint is unauthenticated.
Crowdsourced verification payload.
Run this script on each CodeIgniter 4 application host as a user with read access to the Composer lock file. Invoke with: bash check_cve_2026_63223.sh /var/www/myapp where the argument is the application root directory. No elevated privileges required.
#!/usr/bin/env bash
# check_cve_2026_63223.sh — Detect CVE-2026-63223 in CodeIgniter 4
# Usage: bash check_cve_2026_63223.sh /path/to/ci4/app
set -euo pipefail
APP_ROOT="${1:-}"
if [ -z "$APP_ROOT" ]; then
echo "Usage: $0 /path/to/ci4/app"
exit 3
fi
LOCK_FILE="$APP_ROOT/composer.lock"
if [ ! -f "$LOCK_FILE" ]; then
echo "UNKNOWN — composer.lock not found at $LOCK_FILE"
exit 2
fi
# Extract codeigniter4/framework version from composer.lock
CI_VERSION=$(python3 -c "
import json, sys
with open('$LOCK_FILE') as f:
data = json.load(f)
for pkg in data.get('packages', []) + data.get('packages-dev', []):
if pkg['name'] == 'codeigniter4/framework':
print(pkg['version'].lstrip('v'))
sys.exit(0)
print('NOT_FOUND')
" 2>/dev/null || echo 'PARSE_ERROR')
if [ "$CI_VERSION" = "NOT_FOUND" ]; then
echo "UNKNOWN — codeigniter4/framework not found in composer.lock"
exit 2
fi
if [ "$CI_VERSION" = "PARSE_ERROR" ]; then
echo "UNKNOWN — failed to parse composer.lock (python3 required)"
exit 2
fi
echo "Detected codeigniter4/framework version: $CI_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('$CI_VERSION') < Version('$REQUIRED'):
sys.exit(0) # vulnerable
else:
sys.exit(1) # patched
except Exception:
# Fallback: simple string split comparison
v = list(map(int, '$CI_VERSION'.split('.')[:3]))
r = list(map(int, '$REQUIRED'.split('.')[:3]))
sys.exit(0 if v < r else 1)
" 2>/dev/null; then
echo "VULNERABLE — codeigniter4/framework $CI_VERSION is below $REQUIRED (CVE-2026-63223)"
exit 1
else
echo "PATCHED — codeigniter4/framework $CI_VERSION >= $REQUIRED"
exit 0
fiIf you remember one thing.
codeigniter4/framework:^4.7.4) within 180 days, though given the simplicity of exploitation when preconditions align, pushing the patch to production this week is strongly recommended. Audit your codebase for any upload handler that calls $file->move() with the client-supplied name — those paths are your highest-priority targets. If you cannot patch quickly, the single highest-value compensating control is adding a server-level directive to block PHP execution in upload directories.Sources
- GHSA-mmj4-63m4-r6h5 — CodeIgniter4 Security Advisory
- CodeIgniter4 Security Advisories
- CVE-2026-48062 — ext_in Bypass (GitLab Advisory)
- GitHub Issue #3184 — is_image validation vulnerability
- CodeIgniter Market Share — Enlyft
- CWE-434: Unrestricted Upload of File with Dangerous Type
- CodeIgniter4 on CVE Details
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.