← stauc.eu

CVE-2026-109618 Chain D: Apache can't protect mod_php. nginx can.

2026-10-23 · empirical notes, tested on one Apache 2.4 + mod_php 7.x server

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:

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.

MitigationResultWhy it fails
LimitRequestFields 50Apache deadThe headers that matter live in the multipart body, not in the HTTP request.
LimitRequestBody 1MB413 sent, then crashPHP parses the body during SAPI init, before Apache enforces the limit.
<If Content-Type multipart> + Require all denied403 sent, then crashThe authorization phase runs after mod_php has started parsing.
mod_security phase 1 deny403 sent, then crashmod_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

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

References