· 8 min read
Two critical PHP CVEs land tomorrow. What I'm doing right now.
PHP ships fixes for two critical bugs (CVSS 9.2) on Wednesday at 14:00 UTC, and the details leaked early. The strongest lead points at session upload progress, a feature that's on by default and almost nobody uses. Here's how to switch it off tonight in php.ini, .htaccess, nginx, Caddy, Cloudflare and your WAF.
On Tuesday, the PHP security team posted an advance notice: a security release on Wednesday, October 21 at 14:00 UTC, fixing "two critical vulnerabilities, both rated CVSS v4.0 9.2." All PHP users, it says, "are encouraged to prepare now so they can upgrade as soon as the release is available."
That's unusual. PHP normally just ships. A heads-up the day before means they expect people to be attacked quickly.
They had a reason to hurry: details had already leaked. The bugs are being tracked as CVE-2026-109618 and CVE-2026-109619, and watchTowr, a security firm, told BleepingComputer "they're real." PHP also says it may release earlier if details go public or attacks show up.
So there's one night between now and the patch. Here's what I'm doing with it.

What we know, and what we don't
| Claim | Where it comes from | Status |
|---|---|---|
| Two critical bugs, CVSS v4.0 9.2, fixed Oct 21 at 14:00 UTC | PHP's own notice | Confirmed |
| The bugs are real | watchTowr, to BleepingComputer | Confirmed by a third party |
| No attacks seen in the wild yet | watchTowr | True as of Tuesday |
| Remote code execution in PHP core | Leaked reports, watchTowr | Very likely |
| Affects every PHP from 5.4 on | Sansec, from the banner of a leaked exploit tool called PHP2Shell | Unconfirmed |
| CVSS 10.0 | A deleted Hacker News post, repeated on X | Unofficial: PHP's own score is 9.2 (CVSS v4.0) |
| Fixes only for PHP 8.2 and later | Sansec, and how PHP's support works | Likely |
| It's in session upload progress | Daniele Berardinelli on X, with a memory bug reproduced in a lab build, and Sansec's page tags | Strong lead, not confirmed by PHP |
PHP's notice itself names no versions, no component and no workaround. It says: "Full details, affected versions, and the fixed versions will be published with the release."
If you're on PHP 8.1 or older, don't wait for a patch that probably isn't coming. Those versions are end-of-life, and Sansec says they "will not get an official fix." Plan the upgrade to 8.2 or later now.
The lead: session upload progress
The key finding so far comes from Daniele Berardinelli, a security researcher, who posted on Tuesday that the bug is in PHP's session upload progress code, a feature added in PHP 5.4 that's on by default.
That evening, Berardinelli followed up with more detail: upload progress can serialize session data before PHP's standard request setup has reset the serializer's state. The post includes a screenshot from a lab build of PHP:
- One request crashes partway through serializing ("Allowed memory size … exhausted") and leaves the serializer's state behind in the worker.
- The next request on that worker picks up the same state and touches memory that has already been freed. AddressSanitizer flags it as a use-after-free in
php_add_var_hash.
A use-after-free is the classic way a memory bug becomes code execution, which fits "critical RCE." It's still one researcher's reproduction, not PHP's advisory, but it's the most concrete thing anyone has shown.
The same post says "There is currently no possible mitigation." I read that as: there's no fix until PHP ships one. Berardinelli hasn't said whether turning upload progress off closes the path, and when someone asked exactly that, there was no answer yet. The path described starts with upload progress writing a session. Turning upload progress off stops PHP writing upload progress at all (I tested that with the php.ini, nginx and Caddy steps below), so it's still the first thing I'd do. Only the patch will tell whether it closes every way in.
Then there's Sansec's write-up, which calls the attack PHP2Shell. Its text doesn't name a component. But two details line up:
- The version. Sansec says the leaked exploit tool claims to work against "every PHP release from 5.4.0 on." PHP 5.4 is the release that added session upload progress.
- The page's own tags. The page's search keywords, in its HTML, include
session.upload_progress.enabledandsession.use_strict_mode, two settings the article itself never mentions.
None of that is confirmation, and I'm not going to guess past it.
But here's why I'm switching it off tonight anyway, whatever Wednesday says.
It runs before your code does. When a request arrives as a multipart form (the kind used for file uploads) with a field named PHP_SESSION_UPLOAD_PROGRESS, PHP itself starts writing upload progress into a session. It does that while it reads the request body, before your framework, your router or your login check has run a single line.
It doesn't need your app to use it. I tested what this field makes a stock PHP server do tonight.
PHP's own manual warns about it. It says this feature writes "client-supplied" field and file names into the session, "making any local file inclusion vulnerability trivial to exploit," and that if you don't use upload progress bars, you should "consider disabling this option." That warning didn't come from nowhere (more below).
Almost nobody needs it. It exists to power server-side upload progress bars. Modern upload widgets measure progress in the browser instead. To check your own code, search it for PHP_SESSION_UPLOAD_PROGRESS and upload_progress. If nothing turns up, turning it off changes nothing for your visitors.
It's been here before
This isn't the first time PHP_SESSION_UPLOAD_PROGRESS has come up in security news. Before this week it had one CVE in PHP itself, and several well-known attacks that weren't bugs in PHP at all: they used the feature exactly as designed.
| When | What | CVE |
|---|---|---|
| March 2012 | PHP 5.4 adds session upload progress, switched on by default | — |
| October 2018 | Orange Tsai's "One Line PHP Challenge" at HITCON CTF: upload progress plants attacker-controlled text in a session file, even on an app that never starts a session. Only 3 of 1,816 teams solved it | None |
| February 2020 | A failed upload with cleanup turned off makes PHP read progress data that doesn't exist, and the worker crashes. Fixed in 7.2.28, 7.3.15 and 7.4.3 | CVE-2020-7062 |
| July 2021 | A public exploit and whitepaper, "PHP_SESSION_UPLOAD_PROGRESS Session Data Injection," turn the 2018 trick into a reverse shell. Packet Storm and PwnWiki shared it on X | None |
| September 2021 | A walkthrough of a TyphoonCon CTF challenge shows the same trick as one of its steps | None |
| October 2026 | CVE-2026-109618 and CVE-2026-109619. The lead points here, unconfirmed | Pending |
The 2021 exploit is Exploit-DB 50156, with the write-up in 50157. Orange Tsai's write-up is on his blog.
The attacks from 2018 and 2021 are why the manual carries that warning. They work like this:
- The attacker needs one small bug in your app: a page that can be tricked into including a file from the server's disk (a "local file inclusion"). On its own, that bug usually just leaks files.
- Upload progress gives them a file worth including. A multipart request with
PHP_SESSION_UPLOAD_PROGRESSmakes PHP write the attacker's own text into a session file. The ID is theirs, so they know the file's name:sess_<id>. - They race the cleanup. PHP deletes the progress data when the upload finishes, so they send a slow, large upload and hit the include over and over while it's still running.
- The include runs their text as PHP. A file leak just became remote code execution.
No CVE for steps 2 to 4, because PHP was doing what it was built to do. That's the pattern to notice. This feature has been the bridge from a small bug to a full takeover for years. If this week's bugs really are in it, it's the bridge that's broken this time, with no other bug needed.
The 2021 exploit was tested on PHP 7.3.15, the release that fixed CVE-2020-7062. Patching fixed the crash, but it couldn't fix the trick. Only turning the feature off does that.
Why your app can't stop this
My usual advice for blocking something is a few lines of middleware. Not this time.
By the time Laravel, Symfony or WordPress runs, PHP has already parsed the request body, so the upload progress code has already run. ini_set() in your code is too late for the same reason. The change has to sit either in PHP's own configuration, read before the request starts, or in front of PHP, where the request can be refused before PHP ever sees it.
Do the first one tonight. Like... right now.
We still don't know what the bugs are. But like I said, this feature has been a problem before, and I don't need it. So I'm turning it off.
Step 1: php.ini
This is the step that matters most. Add one line:
session.upload_progress.enabled = Off
Then restart PHP (php-fpm, or Apache if PHP runs inside it). Where the line goes depends on how you run PHP:
| You run | Put it in |
|---|---|
| A Linux server with PHP-FPM | A new file such as /etc/php/8.3/fpm/conf.d/99-no-upload-progress.ini (your version in the path), then sudo systemctl restart php8.3-fpm |
| Apache with mod_php | The same, in /etc/php/8.3/apache2/conf.d/, then restart Apache |
| The official PHP Docker image | RUN echo 'session.upload_progress.enabled=0' > /usr/local/etc/php/conf.d/99-no-upload-progress.ini, then rebuild and redeploy |
| Laravel Forge | Your server's PHP settings: edit the FPM php.ini |
| Laravel Vapor | A php/conf.d/php.ini file in your project, then deploy |
| cPanel shared hosting | MultiPHP INI Editor, or a .user.ini (below) |
Pick the FPM one, not the CLI one. Your server usually has two php.ini files, and php -i on the command line reads the wrong one for websites.
Check it took
Put this in a file in your web root, open it in a browser, then delete it:
<?php var_dump(ini_get('session.upload_progress.enabled'));
You want string(1) "0" or string(0) "". If it says "1", the setting didn't reach the PHP that serves your site.
While you're there: strict mode
That's the other setting in Sansec's tags. Add it on the next line:
session.use_strict_mode = On
It's off by default. With it off, PHP accepts any session ID a visitor sends and creates a session with that name. With it on, PHP throws away IDs it didn't create and hands out a random one.
I tested this as well. Strict mode doesn't stop the upload progress code from running. It only takes away the attacker's choice of session.
That still makes this feature much harder to aim. Without it, an attacker knows which session file they're writing into (sess_ plus the ID they chose, in a folder like /var/lib/php/sessions), and they can hit the same session again and again. With it on, they can do neither. It doesn't stop the code from running, so it's extra hardening on top. Turn off upload progress first.
It's also free for most apps. PHP's manual says enabling it "is mandatory for general session security. All sites are advised to enable this." Laravel doesn't use PHP's own sessions at all, so the setting changes nothing there. The apps it can break are old ones that choose their own session IDs.
Step 2: .htaccess (Apache)
If you can't touch php.ini, the setting can also be changed per folder. Which way depends on how Apache runs PHP.
If PHP runs inside Apache (mod_php), add this to the .htaccess in your web root:
<IfModule mod_php.c>
php_flag session.upload_progress.enabled off
</IfModule>
<IfModule mod_php7.c>
php_flag session.upload_progress.enabled off
</IfModule>
The <IfModule> wrapper matters. On a server where Apache hands PHP off to PHP-FPM (most modern setups and most shared hosts), a bare php_flag line is an unknown command and takes your whole site down with a 500 error.
If Apache hands PHP to PHP-FPM, .htaccess can't change PHP settings. Use a .user.ini file in your web root instead (for Laravel that's public/):
session.upload_progress.enabled = 0
PHP rereads .user.ini files every five minutes, so give it five minutes before you check.
Step 3: nginx
nginx can't look inside a request body by itself, but if it talks to PHP-FPM, it can pass PHP a setting with every request. Add one line to the location that sends requests to PHP, next to your fastcgi_pass:
location ~ \.php$ {
# ...your existing fastcgi_pass and fastcgi_param lines...
fastcgi_param PHP_ADMIN_VALUE "session.upload_progress.enabled=0";
}
Then sudo nginx -t && sudo systemctl reload nginx. I tested this one too: with the line in place, PHP stops tracking upload progress.
Two things to watch:
- If you already have a
PHP_ADMIN_VALUEline, don't add a second one. Put both settings in one, separated by\n:"open_basedir=/var/www\nsession.upload_progress.enabled=0". - Add
try_files $uri =404;to that block if it isn't there. Without it, nginx hands PHP requests for.phpfiles that don't exist, so any made-up URL reaches PHP.
If you'd rather set it on the PHP side, the same switch works in your FPM pool file (for example /etc/php/8.3/fpm/pool.d/www.conf), followed by an FPM restart:
php_admin_flag[session.upload_progress.enabled] = off
Step 4: Caddy
If Caddy talks to PHP-FPM through php_fastcgi, it can pass the same setting:
php_fastcgi unix//run/php/php8.3-fpm.sock {
env PHP_ADMIN_VALUE "session.upload_progress.enabled=0"
}
Like nginx, Caddy has no built-in way to look inside a request body. If Caddy is a reverse proxy in front of someone else's PHP, use a WAF module such as Coraza with the rule in step 6.
Step 5: Cloudflare
This is where it gets uneven: Cloudflare's custom rules can read request bodies, but only on the Enterprise plan. If you have it, go to Security → WAF → Custom rules, create a rule with this expression, and set the action to Block:
(any(http.request.body.multipart.names[*][0] == "PHP_SESSION_UPLOAD_PROGRESS")
or http.request.body.raw contains "PHP_SESSION_UPLOAD_PROGRESS")
Cloudflare only reads the first part of a body (128 KB on Enterprise), so a field buried at the end of a big upload can slip past. That's one more reason step 1 comes first.
On any other plan, a Worker can do it. Workers can read the whole body. Create one, route it to your site (for example example.com/*), and deploy this:
export default {
async fetch(request) {
const type = (request.headers.get('content-type') || '').toLowerCase();
if (!request.body || !type.includes('multipart/form-data')) {
return fetch(request);
}
const body = await request.arrayBuffer();
if (new TextDecoder().decode(body).includes('PHP_SESSION_UPLOAD_PROGRESS')) {
return new Response('Forbidden', { status: 403 });
}
return fetch(new Request(request, { body }));
},
};
It only reads uploads. Every other request passes straight through. It does count against your Workers limits (100,000 requests a day on the free plan), and it holds each upload in memory while it checks, so don't use it on a site that takes very large uploads.
Also keep an eye on Cloudflare's managed rules once details are public. Cloudflare usually ships its own rules for bugs this big, but I can't promise when.
Step 6: Your WAF

ModSecurity, or Coraza (for nginx, Apache or Caddy), with request body inspection on:
SecRequestBodyAccess On
SecRule ARGS_NAMES "@contains PHP_SESSION_UPLOAD_PROGRESS" \
"id:1026001,phase:2,deny,status:403,log,msg:'PHP session upload progress field'"
Use an id that's free in your setup. Keep the stock rule that rejects multipart bodies the WAF can't parse cleanly (MULTIPART_STRICT_ERROR, rule 200003 in modsecurity.conf-recommended). Attackers like bodies that the WAF and PHP read differently.
AWS WAF: add a rule with Inspect: Body, Content type: Plain text, Match type: Contains string, String to match: PHP_SESSION_UPLOAD_PROGRESS, Text transformation: None, and the action Block. AWS WAF only inspects the start of the body (8 KB on a load balancer, 16 KB on CloudFront by default), so set Oversize handling to Continue.
Every WAF reads a limited amount of each body. Treat them all as an early filter, not the patch.
What I did on the Edge
The Edge is the proxy I run in front of a few thousand sites, and plenty of them run PHP. Tonight I built it a rule that refuses any multipart request containing PHP_SESSION_UPLOAD_PROGRESS, on every site, before the request reaches the site or anything else. I'm rolling it out now.
The Edge couldn't look inside request bodies, so I wrote a small handler for it. It reads the first 64 KB of each upload before sending anything on. If the field is in there, the visitor gets a plain 403 and the site never hears about it. If the field is buried deeper in a big upload, the handler holds the last few bytes back as the upload streams, and cuts the upload off before any part of the field name gets through.
Every refusal gets logged, so once the details are public I'll know whether anyone tried it before the patch, and I'll write up what I see.
Tomorrow: patch

The release lands at 14:00 UTC on Wednesday, October 21:
| Where you are | Local time |
|---|---|
| UTC | 14:00 |
| London | 15:00 |
| Central Europe | 16:00 |
| New York | 10:00 |
| Los Angeles | 07:00 |
When it does:
- Read PHP's release notes for the affected and fixed versions. They replace everything in the table above.
- Upgrade every PHP you run: servers, containers, base images, serverless runtimes. If your host manages PHP for you, check that they've done it.
- On PHP 8.1 or older, Sansec says no official fix is coming. Move to a supported branch (8.2 or later), or ask your host whether they'll backport the fix.
- Leave upload progress off afterwards. You didn't need it before, and you won't need it after.
What to look for
There's no public exploit yet, so there's no reliable sign to look for. But if this lead is right, an attack would look like this, and none of it is normal:
- Uploads to odd places. Multipart
POSTs, often with a session cookie, to URLs that don't take uploads or don't exist, especially in bursts across many sites. - PHP crashing. Memory bugs crash workers while people work out how to exploit them. Lines like
child 1234 exited on signal 11 (SIGSEGV)in your PHP-FPM log are worth a look. - Something PHP shouldn't be doing.
sh,curlorwgetrunning under your PHP user, new.phpfiles in folders the web server can write to, or new cron jobs.
If you find signs that someone ran code on a server, treat it as compromised. Take it offline, keep the logs, rotate every secret the app could read (database passwords, API keys, everything in .env), and rebuild it rather than cleaning it.
Tonight's checklist
- Turn it off in
php.ini:session.upload_progress.enabled = Off, then restart PHP-FPM or Apache. - Turn on strict mode while you're there:
session.use_strict_mode = On. - Check it from a browser, not from the command line, then delete the check file.
- Can't edit
php.ini? Use.htaccesswith mod_php,.user.iniwith PHP-FPM, orfastcgi_paramin nginx. - Block the field in front of PHP if you can: a Cloudflare rule or Worker, ModSecurity or Coraza, AWS WAF.
- Patch at 14:00 UTC on Wednesday, and move off PHP 8.1 or older.
I'll update this post when the details are out. I'm @MrNiceTry on X.
Sources: PHP's advance notice; Daniele Berardinelli on the upload progress use-after-free; BleepingComputer on watchTowr's confirmation; Sansec on PHP2Shell; the PHP manual on session configuration; Cloudflare's request body fields.