CVE-2026-109618 Chain D: Apache can't protect mod_php. nginx can.
The problem
CVE-2026-109618 and CVE-2026-109619 are two pre-auth remote code execution bugs in the PHP core (CVSS 9.2, PHP 5.4.0 to 8.5.11, all SAPIs). Chain D is the nasty one: on Apache mod_php it needs no LFI, no unserialize() and no application code at all. A single multipart POST sized to trigger an out-of-memory condition corrupts the Zend engine state.
The mechanism: the OOM causes a longjmp() that skips init_executor, leaving the executor globals uninitialised. mod_php then runs request shutdown unconditionally, even though startup failed, reads those globals, and crashes (or, with heap shaping, executes attacker code).
Who is affected
Having libapache2-mod-php loaded in Apache is enough. You do not need any of these:
- an active PHP site, a single
.phpfile or any application code; - a configured vhost (the default one is enough);
- a specific URL:
/, a 404 path or a static.html/.jpgall work; session.upload_progressbeing On or Off (irrelevant for Chain D).
The reason: mod_php parses the multipart body on every request, during SAPI init, before Apache has decided which file to serve. To check a server:
apache2ctl -M | grep php # any output = vulnerable until patched
PHP-FPM is not affected: FPM terminates the child process on startup failure instead of running shutdown on corrupted state.
Four Apache mitigations that fail
Every one of them was tested. In every case Apache died.
| Mitigation | Result | Why it fails |
|---|---|---|
LimitRequestFields 50 | Apache dead | The headers that matter live in the multipart body, not in the HTTP request. |
LimitRequestBody 1MB | 413 sent, then crash | PHP parses the body during SAPI init, before Apache enforces the limit. |
<If Content-Type multipart> + Require all denied | 403 sent, then crash | The authorization phase runs after mod_php has started parsing. |
mod_security phase 1 deny | 403 sent, then crash | mod_php SAPI init runs before any mod_security phase. |
mod_php handles the multipart body as part of its own initialisation, before any Apache directive gets a say. Apache can answer 403 or 413 correctly and PHP has already crashed.
What works: a reverse proxy in front
nginx reads the request, looks at the Content-Type, and rejects multipart with a 403 itself. The request never reaches Apache.
It does not have to be nginx. Any reverse proxy that parses the HTTP request completely before forwarding it keeps the malformed payload away from mod_php. Cloudflare in front of a static site, for example, answered 400 Bad Request to the OOM trigger and 405 Method Not Allowed to the multipart POST. nginx is simply the one that lets you block multipart explicitly.
Internet → nginx (80/443, SSL termination) → Apache + mod_php (127.0.0.1:80)
|
+- multipart? → 403 (answered by nginx, not forwarded)
The essential part of the nginx config:
map $content_type $block_multipart {
default 0;
"~*multipart" 1;
}
server {
listen 443 ssl;
server_name example.com;
# ssl_certificate / ssl_certificate_key ...
if ($block_multipart) {
return 403 "Multipart requests blocked";
}
location / {
proxy_pass http://127.0.0.1:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 10m;
}
}
Do the same for port 80. Since the check must also cover HTTPS, nginx has to terminate TLS: a proxy that only passes TCP through cannot see the header.
Side effects to handle
- Make Apache listen on
127.0.0.1only, so nobody can bypass nginx. - PHP no longer sees HTTPS. Restore it with
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on. - HTTP→HTTPS redirect rules now loop, because nginx talks plain HTTP to Apache. Add
RewriteCond %{HTTP:X-Forwarded-Proto} !httpsbefore the%{HTTPS} offcondition. - Control panels that regenerate vhosts (ISPConfig, for example) will wipe those edits: put them in the panel's custom template.
- Certificates now live in nginx: reload it from your certbot deploy hook.
- Also set
session.upload_progress.enabled = Offin the web SAPI php.ini (not the CLI one), which covers the other chains.
Verify
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-H "Content-Type: multipart/form-data; boundary=test" \
https://example.com/ # expect 403, from nginx
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/ # expect 200
systemctl is-active apache2 # expect: active
Limits
- Every multipart POST is rejected, so file uploads stop working. A location you exempt from the block stays vulnerable.
- This is a mitigation, not a fix. The real fix is upgrading PHP to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 (or later).
References
- CVE-2026-109618: GHSA-w8hw-w9fx-j79w
- CVE-2026-109619: GHSA-6wmf-4rv5-p3rp