PHP2Shell / CVE-2026-109618
Security advisory · 21 October 2026

When a filename
becomes a shell.

php2shell is a critical, unauthenticated RCE in PHP. This demo shows the publicly documented Chain A: filename → session.upload_progress → LFI → code execution. It is a controlled lab simulation, not an attack on third-party systems.

CVSS 4.0 · 9.2 Critical Pre-auth RCE PHP ≤ 8.5.11 Fix: 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 CVE-2026-109618 / 109619
Authorized labs only. This presentation has no ASLR bypass, no reverse shell and no endpoint for arbitrary commands. It demonstrates the impact of Chain A with a harmless id payload.
Attack chain

Four steps to a shell

Click a card or play the animation. The demo uses a deliberately vulnerable lab with an LFI.

01 · Upload 1

Filename contains PHP

The upload uses a filename such as <?php system($_GET['cmd']); ?>.

02 · Session 2

Upload progress writes it

session.upload_progress stores the filename unescaped in the session file.

03 · LFI 3

The app includes the session

An LFI like include($_GET['page']) loads the file from /tmp/sessions/.

04 · RCE 4

PHP executes the code

The payload runs as the web user — in the lab as www-data, not as root.

Live simulation

Vulnerable vs. patched

The simulation shows the requests and the outcome. The switch changes between PHP 8.3.35 and 8.3.36.

request-flow.log
1
Send multipart uploadPHP_SESSION_UPLOAD_PROGRESS + filename
2
Inspect session file/tmp/sessions/sess_<id>
3
Trigger the LFIindex.php?page=/tmp/sessions/sess_…
4
Read the resultid / whoami / uname
Ready
lab@php2shellvulnerable

        
Configuration

The stock php.ini — and what you can change

These are the relevant values from a stock PHP 8.3.36 build (no application overrides). Distros and Docker images can differ — check php --ini and php -i on the target.

php.ini · stock PHP 8.3.36
; Relevant stock settings (measured, not guessed)
session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix  = upload_progress_
session.use_strict_mode         = Off
session.use_only_cookies        = On
session.cookie_httponly         = Off
session.cookie_secure           = Off
session.cookie_samesite         =
expose_php                      = On
display_errors                  = ""   ; CLI; production php.ini: Off
allow_url_include               = Off
open_basedir                    = ""
The lab is intentionally weaker than stock PHP.
The lab sets session.upload_progress.cleanup=Off and session.use_only_cookies=Off to make the chain deterministic. A stock PHP 8.3 build already has cleanup=On and use_only_cookies=On, which makes this exact Chain A harder — but it does not fix CVE-2026-109618/109619. Patching is still mandatory.
What the values mean
upload_progress writes upload metadata into the session; use_only_cookies decides whether a session ID may arrive in POST/URL; use_strict_mode rejects uninitialized IDs; cleanup deletes progress data after the upload.
DirectiveStock 8.3LabHarden toEffect
session.upload_progress.enabledOnOnOffRemoves the upload-progress write path; may break upload progress UI.
session.upload_progress.cleanupOnOffOnDeletes progress data sooner; reduces the window, not a patch.
session.use_strict_modeOffOffOnRejects attacker-chosen session IDs.
session.use_only_cookiesOnOffOnIgnores PHPSESSID in POST/URL; blocks this chain's SID fixation.
session.cookie_httponlyOffOffOnJavaScript cannot read the session cookie.
session.cookie_secureOffOffOnCookie only over HTTPS.
session.cookie_samesite(empty)(empty)Lax / StrictReduces cross-site request risk.
expose_phpOnOnOffHides the PHP version from response headers.
display_errors"" (CLI)defaultOffPrevents error details from leaking to users; log instead.
allow_url_includeOffOffOffBlocks remote includes (keep this).
open_basedir(empty)(empty)app + session dirLimits which files an LFI can reach; must be tested.

What would have stopped this demo?

session.use_only_cookies = OnStock default. The body PHPSESSID is ignored, so the attacker cannot fix the session ID. The lab disables this on purpose.
session.use_strict_mode = OnEven if an ID is supplied, PHP rejects uninitialized session IDs.
session.upload_progress.enabled = OffNo filename is written into the session file. Defense in depth; may break upload progress.
session.upload_progress.cleanup = OnStock default. Shrinks the race window, but is not a guarantee. Do not rely on it.
LFI / include whitelistThe decisive application fix: never include user-controlled paths. This blocks the chain even with weak session settings.
PHP 8.3.36+The actual fix for CVE-2026-109618/109619. Everything else is defense in depth.

Recommended hardening snippet

; Defense in depth — NOT a replacement for the patch
session.upload_progress.enabled = Off
session.upload_progress.cleanup = On
session.use_strict_mode = On
session.use_only_cookies = On
session.cookie_httponly = On
session.cookie_secure = On
session.cookie_samesite = Lax
expose_php = Off
display_errors = Off
log_errors = On
allow_url_include = Off

; Adjust to your app; may break apps that need other paths
open_basedir = /var/www/html:/tmp

Where to change: php --ini shows the loaded file. On Debian/Ubuntu typically /etc/php/8.3/apache2/php.ini or /etc/php/8.3/fpm/php.ini; in Docker use a file under /usr/local/etc/php/conf.d/. Restart Apache/PHP-FPM after changes.

🧩

Do not break uploads blindly

upload_progress=Off removes progress bars and some upload UX. Test the app; the patch is still the primary fix.

🧱

open_basedir is powerful

It can stop an LFI from reading /etc/passwd, but a wrong path breaks the app. Add only directories the app really needs.

👁️

Log, don't display

display_errors=Off plus log_errors=On avoids leaking paths and stack traces to attackers.

Under the hood

The three ingredients

An LFI, the upload-progress configuration, and a multipart request. No magic — that is what makes it dangerous.

<?php
// Deliberately vulnerable lab — local only!
if (isset($_GET['page'])) {
    include($_GET['page']);
} else {
    echo "php2shell lab";
}
Affected versions

Which versions are safe?

All SAPIs are affected; mod_php is considered especially exposed because the trigger can run before application code.

BranchVulnerableFixStatus
PHP 8.5< 8.5.128.5.12Fix available
PHP 8.4< 8.4.278.4.27Fix available
PHP 8.3< 8.3.368.3.36Fix available
PHP 8.2< 8.2.358.2.35Fix available
PHP ≤ 8.1EOLUpgradeNo longer supported
Controls

What actually helps

Patch first. Everything else is defense in depth, not a replacement for the update.

🛡️

Patch PHP

Upgrade to 8.2.35, 8.3.36, 8.4.27 or 8.5.12+. Restart Apache/PHP-FPM afterwards.

🚫

Disable upload progress

session.upload_progress.enabled=Off reduces the Chain A surface, but does not fix the core CVE.

🧱

Prevent LFI

No dynamic includes. Whitelist files, canonicalize paths, block .. and stream wrappers.

🔒

Harden sessions

use_strict_mode=On, use_only_cookies=On, HttpOnly/Secure/SameSite, short lifetime.

👁️

Detect

Watch for unusual multipart filenames, PHPSESSID in bodies, and includes from /tmp/. Add WAF rules.

🧯

Least privilege

Run PHP as a dedicated user with minimal filesystem and network access. No root, no broad write paths.

✓
Immediate: patch + restartPrioritize internet-facing PHP servers; pull fresh container images and recreate.
✓
Then: LFI inventoryReview every include/require that touches user input.
✓
Monitor: 24–72 hInspect web, auth and process logs for unexpected child processes and files.