---
title: "CVE-2026-109618 & CVE-2026-109619: Mitigating Critical PHP Session Upload Progress Use-After-Free Vulnerabilities Without Upgrading"
date: 2026-10-21
description: "Urgent request-level and web-server mitigations for CVE-2026-109618 and CVE-2026-109619 in PHP's RFC 1867 session upload progress handler (ext/session/session.c) using NGINX, Apache, HAProxy, ModSecurity, and OpenResty when you cannot immediately upgrade PHP."
author: "Daan van Gorkum"
url: "https://dj.vg/posts/cve-2026-109618-cve-2026-109619-php-upload-progress-uaf-mitigation/"
markdown_url: "https://dj.vg/posts/cve-2026-109618-cve-2026-109619-php-upload-progress-uaf-mitigation/index.md"
tags: ["PHP", "Security", "CVE-2026-109618", "CVE-2026-109619", "NGINX", "Apache", "HAProxy", "WAF", "SRE", "Vulnerability-Management", "Linux"]
reading_time: "13 min read"
---

# CVE-2026-109618 & CVE-2026-109619: Mitigating Critical PHP Session Upload Progress Use-After-Free Vulnerabilities Without Upgrading

If you operate PHP workloads in production—whether behind NGINX and `php-fpm`, Apache (`mod_proxy_fcgi` or `mod_php`), HAProxy, or LiteSpeed (`lsphp`)—you need to take action **immediately** to mitigate **CVE-2026-109618** and **CVE-2026-109619**.

Both vulnerabilities are critical **Use-After-Free (UAF)** flaws in PHP's built-in RFC 1867 session upload progress mechanism (`ext/session/session.c`) that allow unauthenticated **Remote Code Execution (RCE)** over a standard HTTP `POST` request.

Worse still:

1. **`session.upload_progress.enabled` is turned `On` (`1`) by default** in upstream PHP and across virtually all major Linux distribution packages (Debian, Ubuntu, RHEL/Rocky, Alpine, and official Docker images).
2. **The vulnerable code executes before your PHP script even starts.** Because `multipart/form-data` parsing happens during the SAPI request startup phase (`rfc1867_post_handler`), application-level middleware, authentication checks, and runtime `ini_set()` calls **cannot** protect you.
3. **Any reachable `.php` endpoint is a target**—even if your application never uses `$_FILES`, never accepts file uploads, and never calls `session_start()`.

In an ideal world, every team would immediately roll out patched PHP binaries across every node. **In reality, not everyone can upgrade PHP right now.** Legacy dependencies, pinned container base images, distro backport delays, third-party binary extensions (such as ionCube or proprietary APM agents), and change freezes mean millions of production PHP instances cannot be upgraded overnight.

If you cannot upgrade PHP today, **you do not have to wait for a binary patch to neutralize this threat.** You can completely sever the attack path right now at the web server, FastCGI, or WAF layer with zero application downtime.

---

## Executive Summary: Take Action Now

| Attribute | Details |
| :--- | :--- |
| **CVE Identifiers** | **CVE-2026-109618**, **CVE-2026-109619** |
| **Vulnerability Class** | Use-After-Free (CWE-416) leading to Pre-Auth Remote Code Execution (RCE) |
| **Affected Component** | PHP `ext/session/session.c` (`php_session_rfc1867_callback`) |
| **Default Exposure** | **Vulnerable by default** (`session.upload_progress.enabled = 1`) |
| **Attack Vector** | `multipart/form-data` HTTP `POST` containing `PHP_SESSION_UPLOAD_PROGRESS` stalled across `session.upload_progress.min_freq` (default `1s`) |
| **Fastest Fix (No Upgrade)** | Force `session.upload_progress.enabled=0` via `php.ini`, `PHP_ADMIN_VALUE` in NGINX/Apache, and block `PHP_SESSION_UPLOAD_PROGRESS` at the edge/WAF |

---

## Understanding CVE-2026-109618 and CVE-2026-109619

To understand why request-level mitigation is mandatory—and why application code cannot block these vulnerabilities—we need to look at how PHP processes `multipart/form-data` uploads under the hood.

When an HTTP client sends a `POST` request with `Content-Type: multipart/form-data`, PHP's SAPI layer invokes `rfc1867_post_handler()` **before** compiling or executing the target `.php` script. If `session.upload_progress.enabled` is enabled (the default) and the multipart stream contains a form field named `PHP_SESSION_UPLOAD_PROGRESS` alongside a session identifier (e.g., `PHPSESSID`), PHP hooks `php_session_rfc1867_callback()` in `ext/session/session.c` into the multipart parser events:

- `MULTIPART_EVENT_START`
- `MULTIPART_EVENT_FORMDATA`
- `MULTIPART_EVENT_FILE_START`
- `MULTIPART_EVENT_FILE_DATA`
- `MULTIPART_EVENT_FILE_END`
- `MULTIPART_EVENT_END`

During the upload, PHP maintains a `php_session_rfc1867_progress` structure holding cached `zval*` pointers directly into the session's progress `HashTable` so it can rapidly update byte counters as chunks arrive:

- `progress->post_bytes_processed` (outer progress counter for the overall request)
- `progress->current_file_bytes_processed` (inner progress counter for the active file part)

Periodically—governed by `session.upload_progress.min_freq` (which defaults to **1 second**)—PHP calls `php_session_rfc1867_update()` to flush progress state to the underlying session storage. When a second update occurs after `session.upload_progress.min_freq` has elapsed, PHP re-reads and decodes the session data, calling `zend_hash_update()` on the session array. That update invokes the hash table destructor (`HashTable.pDestructor = zval_ptr_dtor`) on the previously decoded progress tree—**freeing the underlying `zval` structures while cached pointers in `progress` still reference them**.

### CVE-2026-109618: Stale `progress->post_bytes_processed` Pointer Across Multiple Files

In **CVE-2026-109618**, the outer `zval*` pointer `progress->post_bytes_processed` (resolved via `zend_hash_str_find` on `data["bytes_processed"]`) is cached once when upload progress tracking initializes and is **not refreshed** on a subsequent `MULTIPART_EVENT_FILE_START`.

1. An attacker initiates a `multipart/form-data` request with `PHP_SESSION_UPLOAD_PROGRESS` and begins streaming a first file part, triggering the initial `php_session_rfc1867_update()`.
2. The attacker stalls the TCP transmission for longer than `session.upload_progress.min_freq` (`1` second).
3. Upon resuming the stream, a second `php_session_rfc1867_update()` runs, decoding the session and destroying the previous progress tree via `zval_ptr_dtor`.
4. When a second file part arrives, `MULTIPART_EVENT_FILE_START` refreshes `current_file_bytes_processed`, but leaves `post_bytes_processed` pointing to freed memory.
5. The subsequent `MULTIPART_EVENT_FILE_DATA` callback executes `Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed`, performing a write-after-free into the freed heap bin.

### CVE-2026-109619: Stale `progress->current_file_bytes_processed` Pointer Within a Single File

In **CVE-2026-109619**, the vulnerability triggers within a **single** file upload without requiring a second `MULTIPART_EVENT_FILE_START`:

1. `progress->current_file_bytes_processed` is looked up and cached at `MULTIPART_EVENT_FILE_START`.
2. On each `MULTIPART_EVENT_FILE_DATA` chunk, PHP first writes to both cached pointers (`Z_LVAL_P(progress->current_file_bytes_processed) = offset + length` and `Z_LVAL_P(progress->post_bytes_processed) = post_bytes`) and **then** calls `php_session_rfc1867_update()`.
3. By stalling the upload stream for longer than `session.upload_progress.min_freq` (`1` second) mid-file, the first post-stall `MULTIPART_EVENT_FILE_DATA` event triggers `php_session_rfc1867_update()`, which decodes the session and frees the old progress tree via `zval_ptr_dtor`.
4. Because no new `MULTIPART_EVENT_FILE_START` occurs for the next chunk of the same file, **both** `current_file_bytes_processed` and `post_bytes_processed` remain dangling on the immediately following `MULTIPART_EVENT_FILE_DATA` event, causing dual use-after-free writes into reclaimed Zend memory allocator (`emalloc`) bins.

---

## Why Waiting to Upgrade Is Not an Option (And Why `ini_set()` Fails)

Many engineering teams assume they can mitigate PHP configuration issues by adding `ini_set('session.upload_progress.enabled', '0');` at the top of `index.php` or in a framework bootstrap file.

**That will not work here.**

In PHP's configuration table, `session.upload_progress.enabled` is registered with the `PHP_INI_PERDIR` scope—meaning it can only be set in `php.ini`, `.user.ini`, `php-fpm` pool definitions, `.htaccess`, or web server FastCGI parameters (`PHP_ADMIN_VALUE`). By the time your PHP script executes `ini_set()`, the RFC 1867 multipart stream has **already been parsed** and the use-after-free has already been triggered.

If you cannot immediately upgrade your PHP runtime due to:

- **End-of-Life or LTS Linux distributions** awaiting backported security packages,
- **Legacy PHP applications** (PHP 7.x / 8.0 / 8.1 / 8.2 / 8.3 / 8.4 / 8.5) locked to specific minor versions,
- **Shared hosting or managed appliances** running precompiled `lsphp` or `php-fpm` binaries, or
- **Containerized fleets** where rebuilding and validating hundreds of images takes days,

you must disable the vulnerable subsystem or block the trigger at the **request / web-server layer** today.

---

## Mitigation 1: Zero-Downtime Server-Side Kill Switch (Recommended)

Virtually no modern web application uses PHP's legacy RFC 1867 session upload progress (modern browsers track file upload progress natively on the client side using `XMLHttpRequest.upload.onprogress` or the Fetch API).

Disabling `session.upload_progress.enabled` completely bypasses `php_session_rfc1867_callback()` in `ext/session/session.c`, neutralizing both **CVE-2026-109618** and **CVE-2026-109619** with zero functional impact on file uploads or sessions.

### Option A: NGINX + PHP-FPM (`fastcgi_param PHP_ADMIN_VALUE`)

You do not even need to touch `php.ini` or restart `php-fpm` workers if you inject `PHP_ADMIN_VALUE` directly from NGINX. Using `PHP_ADMIN_VALUE` (instead of `PHP_VALUE`) ensures that no `.user.ini` file can re-enable the setting:

```nginx
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm.sock; # or your upstream pool

    # Neutralize CVE-2026-109618 & CVE-2026-109619 across all requests
    fastcgi_param PHP_ADMIN_VALUE "session.upload_progress.enabled=0";
}
```

Test and reload NGINX with zero downtime:

```bash
nginx -t && systemctl reload nginx
```

### Option B: Apache (`mod_proxy_fcgi` or `mod_php`)

If you run Apache fronting PHP-FPM via `mod_proxy_fcgi`, pass `PHP_ADMIN_VALUE` in the environment:

```apache
<IfModule mod_proxy_fcgi.c>
    SetEnv PHP_ADMIN_VALUE "session.upload_progress.enabled=0"
</IfModule>
```

If you run legacy `mod_php` (or LiteSpeed reading Apache configuration directives), lock the flag down with `php_admin_flag`:

```apache
<IfModule mod_php.c>
    php_admin_flag session.upload_progress.enabled off
</IfModule>
```

Gracefully reload Apache:

```bash
apachectl configtest && systemctl reload apache2
```

### Option C: HAProxy Native FastCGI (`fcgi-app`)

If HAProxy communicates directly with `php-fpm` backends over FastCGI (`proto fcgi`), inject `PHP_ADMIN_VALUE` inside your `fcgi-app` definition:

```haproxy
fcgi-app php-fpm-app
    docroot /var/www/html
    index index.php
    path-info ^(/.+\.php)(/.*)?$
    # Disable PHP session upload progress across all FastCGI requests
    set-param PHP_ADMIN_VALUE "session.upload_progress.enabled=0"

backend php_fpm_nodes
    mode http
    use-fcgi-app php-fpm-app
    server fpm1 127.0.0.1:9000 proto fcgi check
```

Validate and reload HAProxy cleanly:

```bash
haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl reload haproxy
```

### Option D: Global `php.ini` and PHP-FPM Pool Configuration

To enforce the mitigation permanently across CLI, FPM, and all SAPIs on the host, drop a dedicated configuration snippet into your PHP `conf.d` directory (e.g., `/etc/php/*/fpm/conf.d/99-disable-upload-progress.ini`):

```ini
; Mitigate CVE-2026-109618 and CVE-2026-109619
session.upload_progress.enabled = 0
```

Or lock it inside your PHP-FPM pool configurations (`/etc/php/*/fpm/pool.d/www.conf`):

```ini
php_admin_flag[session.upload_progress.enabled] = off
```

---

## Mitigation 2: Edge & WAF Request Filtering (Block `PHP_SESSION_UPLOAD_PROGRESS`)

In large multi-tenant environments, reverse-proxy fleets, or appliances where you cannot immediately modify backend FastCGI parameters, you should block any incoming `multipart/form-data` request attempting to pass the `PHP_SESSION_UPLOAD_PROGRESS` field (or whatever custom name is configured in `session.upload_progress.name`).

### HAProxy (L7 Request Body Inspection)

When HAProxy fronts your web tier over HTTP/HTTPS, you can enable `option http-buffer-request` in your `frontend` or `backend` and reject `multipart/form-data` requests containing `PHP_SESSION_UPLOAD_PROGRESS` before they ever reach an upstream server:

```haproxy
frontend https_edge
    bind :443 ssl crt /etc/haproxy/certs/
    mode http

    # Buffer the incoming request body so ACLs can inspect multipart fields
    option http-buffer-request

    # Match POST multipart/form-data requests containing PHP_SESSION_UPLOAD_PROGRESS
    acl is_post method POST
    acl is_multipart req.hdr(Content-Type) -i -m sub multipart/form-data
    acl has_upload_progress req.body -m sub PHP_SESSION_UPLOAD_PROGRESS

    http-request deny deny_status 403 if is_post is_multipart has_upload_progress

    default_backend web_backends
```

> **Tip:** HAProxy inspects `req.body` up to `tune.bufsize` (default `16384` bytes). Because RFC 1867 requires `PHP_SESSION_UPLOAD_PROGRESS` to appear **before** the uploaded file part in the multipart stream so PHP knows to initialize tracking at `MULTIPART_EVENT_FILE_START` (or if you also increase `tune.bufsize 65536` in `global`), inspecting the buffered request body reliably catches exploit attempts at the edge.

### ModSecurity (Apache, NGINX, or OWASP CRS)

Add the following rules to your custom ModSecurity configuration (e.g., `REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` or `custom-rules.conf`):

```apache
# Rule 1: Block parsed POST form fields named PHP_SESSION_UPLOAD_PROGRESS
SecRule REQUEST_METHOD "@streq POST" \
    "id:1000101,\
    phase:2,\
    block,\
    status:403,\
    log,\
    msg:'CVE-2026-109618/CVE-2026-109619: Blocked PHP_SESSION_UPLOAD_PROGRESS field',\
    chain"
    SecRule ARGS_POST_NAMES "@contains PHP_SESSION_UPLOAD_PROGRESS" ""

# Rule 2: Inspect raw multipart/form-data body for the Content-Disposition field name
SecRule REQUEST_BODY "@rx name=[\"']?PHP_SESSION_UPLOAD_PROGRESS[\"']?" \
    "id:1000102,\
    phase:2,\
    block,\
    status:403,\
    log,\
    msg:'CVE-2026-109618/CVE-2026-109619: Blocked raw multipart PHP_SESSION_UPLOAD_PROGRESS'"
```

### OpenResty / NGINX with `lua-nginx-module`

If you terminate traffic at OpenResty or NGINX with Lua enabled, you can inspect `multipart/form-data` bodies at the edge and reject matching requests with `403 Forbidden` before they ever reach an upstream PHP worker:

```nginx
location / {
    access_by_lua_block {
        local content_type = ngx.req.get_headers()["content-type"]
        if content_type and string.find(content_type:lower(), "multipart/form-data", 1, true) then
            ngx.req.read_body()
            local body = ngx.req.get_body_data()
            if not body then
                local body_file = ngx.req.get_body_file()
                if body_file then
                    local f = io.open(body_file, "rb")
                    if f then
                        body = f:read(65536)
                        f:close()
                    end
                end
            end
            if body and string.find(body, "PHP_SESSION_UPLOAD_PROGRESS", 1, true) then
                ngx.log(ngx.ERR, "Blocked CVE-2026-109618 / CVE-2026-109619 exploit attempt")
                return ngx.exit(ngx.HTTP_FORBIDDEN)
            end
        end
    }

    # Standard proxy_pass or fastcgi_pass configuration below
}
```

---

## Mitigation 3: Reverse Proxy Request Buffering & Timeout Hardening

Both **CVE-2026-109618** and **CVE-2026-109619** require the multipart upload stream to span longer than `session.upload_progress.min_freq` (default `1` second) while PHP is actively reading the request body, forcing a mid-stream `php_session_rfc1867_update()` cycle.

When your reverse proxy **fully buffers** the client request body on disk/memory before opening the FastCGI or HTTP connection to the PHP backend, PHP receives the entire multipart payload in a single local burst in under a millisecond—preventing the 1-second `min_freq` window from ever elapsing during `rfc1867_post_handler()`.

### NGINX Request Buffering

Ensure that `fastcgi_request_buffering` and `proxy_request_buffering` are set to `on` (the NGINX default, though sometimes disabled by operators for large file upload routes):

```nginx
http {
    # Buffer the entire client request body before forwarding to PHP-FPM
    fastcgi_request_buffering on;
    proxy_request_buffering on;

    # Enforce strict timeouts against intentionally stalled request bodies
    client_body_timeout 10s;
}
```

### HAProxy Request Buffering & Stall Timeouts

In HAProxy, enabling `option http-buffer-request` alongside a strict `timeout http-request` forces HAProxy to wait for the buffered payload within the timeout window rather than trickling stalled chunks directly into an open PHP worker:

```haproxy
defaults
    mode http
    option http-buffer-request
    timeout http-request 5s
    timeout client-fin 5s
```

### Apache `mod_reqtimeout`

If Apache accepts client connections directly without a buffering reverse proxy in front of it, enable `mod_reqtimeout` to drop connections that intentionally stall mid-body:

```apache
<IfModule mod_reqtimeout.c>
    RequestReadTimeout header=10-20,MinRate=500 body=10,MinRate=1000
</IfModule>
```

> **Note:** Request buffering and timeouts are defense-in-depth measures. Always pair them with **Mitigation 1** (`session.upload_progress.enabled=0`) or **Mitigation 2** (WAF filtering), as large uploads over high-latency disks could still cross the 1-second threshold if upload progress remains enabled.

---

## Auditing Your Fleet Right Now

To quickly check whether any PHP installations or FPM pools on a Linux host still have `session.upload_progress.enabled` active, run this one-liner audit across your servers:

```bash
for bin in $(which php php-fpm php-fpm8.1 php-fpm8.2 php-fpm8.3 php-fpm8.4 php-fpm8.5 lsphp 2>/dev/null | sort -u); do
    echo "=== Auditing: $bin ==="
    "$bin" -i 2>/dev/null | grep -E "session\.upload_progress\.(enabled|name|min_freq)"
done
```

And inspect all `php.ini` and FPM pool files on disk:

```bash
grep -rn "upload_progress" /etc/php* /usr/local/etc/php* /usr/local/lsws/lsphp* 2>/dev/null
```

### Verifying That the Mitigation Is Active

Once you have applied `session.upload_progress.enabled=0`, verify via a temporary diagnostic check or `php -i` that both the **Local Value** and **Master Value** report `Off` (`0`):

```text
session.upload_progress.enabled => Off => Off
```

If you deployed the WAF or OpenResty rule, test against a staging endpoint with a benign `multipart/form-data` request containing the `PHP_SESSION_UPLOAD_PROGRESS` field and confirm that your edge immediately returns `HTTP/1.1 403 Forbidden`:

```bash
curl -i -X POST "https://staging.example.com/index.php" \
  -F "PHP_SESSION_UPLOAD_PROGRESS=test" \
  -F "file=@/dev/null;filename=test.txt"
```

---

## Frequently Asked Questions (FAQ)

### Does disabling `session.upload_progress.enabled` break normal file uploads in PHP?
**No.** Standard file uploads via `$_FILES` and `move_uploaded_file()` are handled by PHP's core RFC 1867 parser (`file_uploads = On`) and are completely independent of `session.upload_progress.enabled`. Disabling session upload progress only stops PHP from writing real-time byte counters into `$_SESSION["upload_progress_..."]` while the upload is in flight.

### Are applications that don't use `session_start()` or file uploads still vulnerable?
**Yes.** When `session.upload_progress.enabled=1` is set (the default) and a `POST` request contains `PHP_SESSION_UPLOAD_PROGRESS`, PHP automatically initializes the session module during `rfc1867_post_handler()` before executing the target PHP script—even if that script never calls `session_start()` or touches `$_FILES`.

### Can I fix CVE-2026-109618 and CVE-2026-109619 using `ini_set()` in my PHP code?
**No.** Because `rfc1867_post_handler()` runs during the SAPI request initialization phase prior to script execution, the memory corruption occurs before the first line of your `.php` script is reached. You must disable `session.upload_progress.enabled` in `php.ini`, `php-fpm` pool configs, or via `PHP_ADMIN_VALUE` in NGINX/Apache.
