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.


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:

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:

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:

<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:

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

Gracefully reload Apache:

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:

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:

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):

; 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):

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:

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):

# 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:

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):

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:

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:

<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:

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:

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):

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:

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.