Sanniv Khare 5 min read

CVE-2026-115560: checking WordPress for the PHP session RCE

CVE-2026-115560: checking WordPress for the PHP session RCE

Two days after the 21 October PHP security release, php.net posted an informational advisory for CVE-2026-115560, a second session-handler code-execution bug that the 21 October patches do not fix. There is no fixed version yet; php.net says it will ship in the next regular bugfix release. The useful question this week is not “how bad is it” but “does anything on my server actually meet the conditions”. We run PHP 8.5.12 on a CloudPanel VPS serving WordPress (with our own Paperkite plugin) and Laravel, so we ran the check across our stack and then shut the door regardless.

The short version: CVE-2026-115560 only fires when three things are true at once, a default WordPress install meets none of them, and one line in the FPM pool makes the question moot. We verified the audit method on PHP 8.3.6 with nginx 1.24.0 in a Linux container on 30 October 2026, against WordPress 7.1.4 core and the 30 most-installed plugins.

The three conditions

Per the php.net advisory (23 October 2026), all three must hold for the bug to be reachable:

  1. Configuration sets session.serialize_handler to php_binary or php_serialize.
  2. Application code switches it back to php at runtime with ini_set(), then calls session_start().
  3. session.upload_progress.enabled is still on (it is, by default).

If the handler stays at its default php and nothing flips it at runtime, conditions one and two never apply. That is the case we expected for WordPress, which does not use PHP’s native sessions at all, but the runtime switch could hide in a Composer library under vendor/, so we checked rather than assumed.

The audit, two greps

The whole check is two grep passes: one over config, one over every PHP file including vendored code. We wrapped them in a script so it reports per tree.

#!/usr/bin/env bash
# session-audit.sh <dir> ... : who mentions serialize_handler, who flips it, who starts a session
for root in "$@"; do
  sh=$(grep -rl  --include='*.php' --include='.htaccess' --include='.user.ini' 'serialize_handler' "$root" 2>/dev/null | wc -l)
  ini=$(grep -rlE --include='*.php' "ini_set\s*\(\s*['\"]session\.serialize_handler" "$root" 2>/dev/null | wc -l)
  ss=$(grep -rlE --include='*.php' '\bsession_start\s*\(' "$root" 2>/dev/null | wc -l)
  printf '%-30s serialize_handler:%s ini_set(handler):%s session_start:%s\n' \
    "$(basename "$root")" "$sh" "$ini" "$ss"
done

For config, the one pass that matters is:

grep -rn "serialize_handler" /etc/php* /etc/apache2 /etc/nginx 2>/dev/null

What we found

Across WordPress 7.1.4 core and 30 current plugins, not a single file calls ini_set() on the serialize handler, and nothing sets serialize_handler in config. That is condition two gone, which is enough on its own.

Terminal capture of the session audit across WordPress core and 30 plugins, every count zero for serialize_handler and ini_set

Running the audit across WordPress core and 30 plugins: every one reads zero on serialize_handler and ini_set(handler).

A handful of files do call session_start(), but every one is either a translation string in core’s Site Health page or a bundled third-party library (phpseclib’s Crypt/Random, Hybridauth in Loginizer, the AWS SDK’s DynamoDB session handler in W3 Total Cache). None of them touches the serialize handler, so condition two is still absent and session_start() on its own is harmless here.

Laravel is simpler still: its file, database and redis session drivers do not use PHP’s native session mechanism and never call session_start(), so the precondition cannot be met by the framework itself. We note this from Laravel’s documented session handling; we did not grep our deployed Laravel tree in this run, so confirm your own application and package code with the same two greps.

Shutting the door anyway

Even with the audit clean, the cheapest safe state is to remove condition three. Set session.upload_progress.enabled=0, and set it where application code and .user.ini cannot put it back, which is exactly what php.net and Red Hat’s RHSB-2026-013 advise: use php_admin_value in the FPM pool, not .user.ini or ini_set().

; /etc/php/8.x/fpm/pool.d/<pool>.conf
php_admin_value[session.upload_progress.enabled] = 0

The distinction between php_admin_value and php_value is the whole point, so we proved it. With the directive as an admin value, a .user.ini in the docroot asking for enabled = 1 and a runtime ini_set() both fail:

Terminal output showing session.upload_progress.enabled stays 0 and ini_set returns false under php_admin_value

Under php_admin_value the pool wins: the .user.ini override is ignored and ini_set() returns false.

Swap that one line to php_value and the same .user.ini flips it straight back to 1 and raises post_max_size to whatever it likes. Use php_admin_value. After the change, reload is not enough for a security directive; restart the pool so every worker picks it up (systemctl restart php8.5-fpm), then confirm with php -i | grep upload_progress.

What to actually do this week

Run the two greps on every site tree, vendor/ included. If both come back empty, you were never exposed to CVE-2026-115560; if the ini_set() grep hits, read that file before anything else. Either way, set session.upload_progress.enabled=0 as a pool admin value and restart, so the question stops depending on what some dependency does at runtime. This is the companion to the 21 October patch itself; if you have not applied that, do it first.

Need this built? Let’s talk.

We build and run apps for Mac, TV, mobile and the web, and write about what we learn.