Remove adsMember
Theme: SystemContrast: Standard
On this page
  1. Why now
  2. Leaving through the window
  3. A toy version
  4. Who’s exposed
  5. Patching
  6. What we don’t know yet
  7. Detection, while signatures are thin

PHP2Shell: when PHP's error exit leaves a mess behind

By 9 min readExpert
Copy article
Share this article

A PHP worker has been serving requests for six hours without a restart. Request 41,872 runs out of memory halfway through and dies. The worker shrugs and picks up request 41,873. That’s on purpose: it’s how PHP keeps its “fresh process per request” feel inside long-lived workers.

On 21 October 2026 the PHP project shipped fixes for two critical flaws that live in exactly that gap, between “request died” and “next request starts”. They’re CVE-2026-109618 and CVE-2026-109619, and researchers have nicknamed the pair PHP2Shell [1][3][4]. If you only read one line: update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or to your vendor’s backport [1][3].

Why now

The fixed releases and both CVEs went public on 21 October 2026, each scored 9.2 on CVSS v4.0 [1][3]. One vendor reports that details leaked first and PHP moved its disclosure forward [4]. As of 30 October the Dutch NCSC tracks 929 products, 217 of them marked vulnerable and 165 fixed [3]. That’s a lot of web infrastructure in an unfinished state, so it’s worth understanding the bug class and not just the patch number.

Timeline of the PHP2Shell disclosure

Leaving through the window

When a request hits an out-of-memory error or a fatal error, PHP aborts it by calling longjmp() to jump back out of whatever C code was running [1]. If you haven’t written C in a while: longjmp() unwinds the stack without running the cleanup code of the functions it skips. Whatever those functions were going to free, reset or initialise just doesn’t happen. It’s pulling the fire alarm and climbing out of the window. Quick, but nobody locks the filing cabinet.

The vendor write-ups describe two different ways that shortcut goes wrong [1][4]:

CVE-2026-109618 CVE-2026-109619
Trigger Out-of-memory (OOM) error Fatal error
What is left behind Request memory in an uninitialised state, because some contexts did not account for the OOM jump Global memory in a bad state, still pointing at memory that is freed at request end
Who trips over it Cleanup of a later request reads or branches on that memory A later request reads or branches on the freed memory
Stated outcome Remote code execution; several OOM strategies can trigger it repeatedly Remote code execution; multiple attack techniques possible

Both come down to the same thing: state owned by a request outlives that request. The first request isn’t the one that gets exploited; it’s the setup. The damage lands in a neighbouring request on the same worker.

How a longjmp bailout leaves stale state for the next request

A toy version

This isn’t PHP source. It’s a toy that shows why skipping cleanup is dangerous:

c
static char *g_buf;               /* global, request-scoped by convention */

void handle_request(void) {
    g_buf = request_alloc(4096);  /* owned by the request arena */
    if (do_work() == OOM)
        longjmp(bailout, 1);      /* skips the line below */
    g_buf = NULL;                 /* never runs on the error path */
}
/* arena freed at request end; g_buf still holds the old address */

Once the arena is released, g_buf points into memory that can be handed out again. If a later request, or its cleanup, reads through that pointer or decides what to do based on what’s there, an attacker who can shape what lives at that address gets a say in control flow. Classic use-after-free. The vendor write-ups describe the read-and-branch step but not the exact exploit chain, and I won’t guess at it [1][4].

What makes this unusual for a PHP bug: there’s no unserialize() of attacker data, no eval, nothing in your application at all. The flaw is in the engine’s request lifecycle. That’s why the advisory says every PHP installation serving web requests is at risk, whatever the SAPI [3].

Who’s exposed

PHP names PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed, and rates mod_php highest, because an unauthenticated request can trigger exploitation before any application code runs [3]. Persistent workers are the interesting case: the stale state outlives a single request, so clean application code doesn’t save you.

PHP also lives inside appliances, network devices and management interfaces where you can’t just run apt upgrade. Those need vendor updates, and the NCSC product list is where to look [3].

Patching

Find every PHP runtime you have: containers, base images, appliances, CI runners that serve anything. Check the version, update to a fixed release or vendor backport, restart the workers so old binaries aren’t still loaded, and check again with the same command:

bash
php -v                                  # CLI binary
php-fpm -v                              # the FPM binary can differ from the CLI
apache2ctl -M 2>/dev/null | grep php    # mod_php present?
docker run --rm your-image php -v       # do not trust the tag name

Fixed upstream releases:

Branch Fixed in
8.5 8.5.12
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35

Upstream no longer supports PHP 8.1 or older. Distributions and LTS vendors may ship backports, and PHP published minimal source backports for 7.4, 8.0 and 8.1 for people who build from source [1][3]. Read your distribution’s changelog: a package version that looks old can still carry the fix.

Do it today for your container images too. An image pinned to a patch tag from before 21 October is exactly the thing that stays vulnerable for months because nobody owns it.

On an end-of-life branch

Two practitioner write-ups, from Intelvue and ComputingForGeeks, cover the branches upstream no longer supports [5][6]. Both come from people who sell or run this work, so check the details against your distribution’s own notes.

First, what the backport is. For 7.4, 8.0 and 8.1, PHP published “minimal mitigation for EOL version” commits. They carry the core fix (“Avoid state corruption via bailout”) but differ from the full fix in three ways [6]. Session upload progress is switched off in the code whatever php.ini says, so the setting can still read On while the feature is dead. The full fix’s memory_hard_limit_percent setting doesn’t exist. And every memory_limit overrun ends the process immediately, so shutdown functions never run; behind PHP-FPM the worker dies and visitors get a 502 or 503 [6].

Intelvue’s five steps [5], with the ComputingForGeeks details folded in [6]:

  1. Find every PHP version and how it runs. php -v, but also ls /etc/php/ /opt/remi/ and systemctl list-units '*php*fpm*'. The CLI version is often not the one serving traffic, so check each site’s FPM pool or Apache mod_php module [5]. If an application is encoded with ionCube or its vendor only certifies PHP 8.1, you may have to patch that branch instead of moving up [6].
  2. Install the patched package for your branch: apt install --only-upgrade 'php8.1*', dnf upgrade --refresh 'php*', dnf upgrade -y 'php81-*' for a Remi collection, or EasyApache 4 and CustomBuild on cPanel and DirectAdmin. Then restart every pool, and Apache if you use mod_php [5]. Stay with the packager you already use [6]. Red Hat retired the RHEL 9 php:8.1 and RHEL 8 php:8.0 streams, so RHEL and Rocky servers on those get no vendor fix and have to move to Remi’s matching stream [6]. Verify by changelog, not version number. Patched builds keep the same version string, and the ini checks answer the same as on a vulnerable build. For example zgrep CVE-2026-109618 /usr/share/doc/php8.1-fpm/changelog.Debian.gz or rpm -q --changelog php81-php-fpm | head -3 [5][6]. On AlmaLinux the changelog is the only proof [6].
  3. Turn off session upload progress: session.upload_progress.enabled = Off in php.ini, and as php_admin_flag in each FPM pool, then restart. PHP recommends keeping it off until the next release [3][5]. Only progress bars that read from the PHP session stop working; uploads themselves are fine [5].
  4. Don’t remove the memory limit. Early advice to set memory_limit = -1 to dodge the out-of-memory trigger lets one request eat all the server’s memory. Keep a sensible limit [5]. After patching, set catch_workers_output = yes in the pool and count exited with code 1 lines in the FPM master log for a few days. A pool where that keeps climbing needs a higher memory_limit [5][6].
  5. If there’s no patched package, shield the server. For PHP 7.3 and older PHP published nothing, though a few packagers patched on their own (DirectAdmin, Freexian ELTS for Debian, Ubuntu Pro); Remi’s 7.2 and 7.3 builds have no fix [6]. Without a package: put the server behind a WAF with PHP2Shell rules in blocking mode, block multipart/form-data on routes that don’t take uploads, restrict admin panels to a VPN or allow-listed addresses, and take it offline if none of that is possible [5].

ComputingForGeeks adds that the backport closes PHP2Shell and nothing else, and the next PHP security release may come with no backport at all. Plan the application upgrade now. It also reports that CISA’s alert told organizations on PHP 8.1 or earlier to take PHP systems offline until patched or updated [6].

Upload progress, and stopgaps

On 23 October PHP published a separate informational advisory suggesting session.upload_progress.enabled=0 until the next regular bugfix release, to guard against an unlikely RCE vector tracked as CVE-2026-115560. A full advisory will come with that release. The NCSC repository doesn’t track it separately, so patching alone doesn’t retire it [3].

Where you really can’t patch yet, PHP’s interim advice is to disable session.upload_progress, restart workers, and review the guidance on memory_limit, post_max_size and, on PHP 8.5, fatal_error_backtraces [3]. Both CVEs need an OOM or a fatal error to fire, so limits like these plausibly take away some of the attacker’s options. They’re a speed bump, not a fix. So is a WAF rule: Fastly describes its signatures for structural oddities in request bodies as virtual patching until the real patch lands [1].

What we don’t know yet

I’d rather be honest here than loud.

The affected version range is disputed. Fastly says all of PHP 5 through 8 is exposed [1]. An early leak claimed something similar alongside CVSS 10.0 scores; SOC Prime points out that the official 9.2 contradicts that, and that no official vendor advisory had confirmed the range when they wrote [4]. Upstream fixed releases exist only for 8.2 to 8.5, with minimal backports for 7.4, 8.0 and 8.1 [3]. Treat “every version ever” as a vendor-blog claim, not a fact, and let your distribution’s tracker settle it for your package.

Exploitation is reported, narrowly. Unit 42 says the flaws are being actively exploited against internet-exposed PAN-OS management interfaces. It cites Dream Research, which reports attackers chaining PHP2Shell with a Linux kernel privilege-escalation bug to get root, and says it independently verified the same attacker’s tooling [2]. The same brief retracts an earlier claim: post-compromise activity at a European government agency was first attributed to PHP2Shell, but Unit 42 now can’t confirm that initial access involved these CVEs [2]. So exploitation is reported for one product family, not established for PHP deployments in general. The NCSC tells operators to watch for public exploit activity [3].

And there’s no public exploit chain. The advisories describe the bug class but not a reproducible chain. Anything past that is speculation, mine included.

The Unit 42 brief was last updated on 29 October [2]. Read it together with the NCSC list for the current status; this article can’t see what changed after it was written.

Detection, while signatures are thin

With no public indicators, behaviour is the best signal you have. SOC Prime suggests watching for PHP or web server processes unexpectedly spawning shells, interpreters or download tools [4]. That’s ordinary web-shell hygiene, worth having anyway: php-fpm, apache2 or httpd as the parent of sh, bash, curl or wget. Writes by the web user into the webroot or temp directories, followed by execution.

Also look for bursts of worker crashes, OOM kills or fatal errors in the PHP log from a single client shortly before an odd process spawn. That order of events would fit the bug class (fail once to prime the state, then follow up), but to be clear, no source describes it as an observed attack pattern.

“Fresh state per request” sounds like a property PHP just has. It isn’t. Someone has to maintain it by hand, on every error path, forever, and this time two paths slipped.

Share this article

Share this article

Sources

  1. Fastly: Critical RCE vulnerabilities in Core PHP, CVE-2026-109618 and CVE-2026-109619123456789
  2. Unit 42: Threat Brief on CVE-2026-109618 and CVE-2026-109619123
  3. NCSC-NL php2shell: operational information12345678910111213
  4. SOC Prime: CVE-2026-109618 and CVE-2026-109619 analysis123456
  5. Intelvue: PHP2Shell fix for end-of-life PHP, step by step12345678910
  6. ComputingForGeeks: PHP2Shell on end-of-life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube)123456789101112

Keyboard shortcuts

Close

Everywhere

Search tools and actions
Ctrl K
Show this help
?
Switch theme
Ctrl Shift L
Pin the selected tool (in search)
Ctrl P

On a tool

Send the output to the first suggestion
Ctrl ↵
Open a file into the input
Ctrl O
Copy the output
Ctrl Shift C
Close searchesc
toolsBase64 encoder and decoderEncode text to Base64 or decode Base64 back to text, with standard and URL-safe alphabets.EncodingJSON formatter and validatorFormat, minify and validate JSON, with the exact line and column of any error.FormattingRegex testerTest JavaScript regular expressions with live highlighting, capture groups and a plain-language explanation.TextUUID and ULID generatorGenerate UUID v4, UUID v7 and ULID in bulk, or paste an ID to see its version, variant and timestamp.GeneratorsUnix timestamp converterConvert Unix timestamps to dates and back, in any time zone, with ISO 8601, RFC 2822 and relative time.Date and timeHash and HMAC generatorCalculate MD5, SHA-1, SHA-2, SHA-3, BLAKE2, BLAKE3 and CRC-32 hashes of text or a file, with optional HMAC.SecurityPassword generator and strength checkerGenerate random passwords, passphrases and PINs, or check how strong a password is.SecurityJWT decoder and verifierDecode a JSON Web Token, check its expiry and claims, and verify its signature with a secret or public key.SecurityCIDR and IP subnet calculatorCalculate the range, mask and size of IPv4 and IPv6 networks, split them into subnets, and check what contains or overlaps what.NetworkCron expression explainerRead a cron expression in plain language, see its next runs in any time zone, and tidy it into its standard form.Date and timeSort linesSort lines of text A to Z or Z to A, in natural order (file2 before file10), by code point, by number or by length.TextRemove duplicate linesRemove duplicate lines from a list, keep the first or last copy, ignore case or spaces, or drop every line that repeats.TextCompare lists and textCompare two lists line by line: find what is in both, only in one, or in either, or see a marked line diff side by side.TextText case converter and slugifyConvert text to camelCase, snake_case, kebab-case, Title Case and more, or turn a title into a URL slug with accents handled.TextURL encode, decode and parsePercent-encode and decode text for URLs, or take a URL apart into scheme, host, path and query and edit the query as a table.EncodingValidators: IBAN, BSN, VAT, card and ISBNCheck an IBAN, Dutch BSN, credit card number, EU VAT number, email address, URL or ISBN, with the reason when it fails. One value per line.ValidationHTML entity encoder and decoderEscape text for HTML with named, decimal or hex entities, or turn entities such as & and é back into characters.EncodingEscape and unescape stringsEscape text for a JSON or JavaScript string, a SQL string, a CSV field, a regular expression or a shell word, or undo the escaping.EncodingEAN, UPC and GTIN check digitsCheck the check digit of an EAN-8, EAN-13, UPC-A or GTIN-14, add a missing one, read the country from the GS1 prefix and convert between ISBN-10 and EAN-13. One code per line.ValidationTest data generatorGenerate valid-looking test IBANs, BSNs, EAN barcodes and EU VAT numbers in bulk. They pass the checks, but belong to no one.GeneratorsSQL formatter and minifierFormat SQL so each clause sits on its own line, or minify it onto one line, for MySQL, PostgreSQL, SQLite, SQL Server and BigQuery.FormattingNumber base converterConvert numbers between binary, octal, decimal, hex and any base from 2 to 36, with big numbers, two's complement and a bit view.EncodingYAML, TOML and JSON converterConvert between JSON, YAML and TOML, or validate and pretty-print a document, with the line and column of every error.FormattingCSV and JSON converterConvert CSV to a JSON array of objects or arrays, or JSON back to CSV. Detects the delimiter and reads Dutch Excel files with semicolons and decimal commas.DataGlob and .gitignore testerTest glob patterns and .gitignore rules against a list of paths and see which paths match and which rule matched them.TextColour converterConvert a colour between HEX, RGB, HSL, HWB and OKLCH, check its WCAG contrast against a background, or build a Material 3 tonal palette from it.Formattingchmod calculatorConvert Unix file permissions between octal (755), letters (rwxr-xr-x), ls -l and chmod clauses, with a checkbox grid and the setuid, setgid and sticky bits.SecurityUser-agent parserRead a user-agent string: browser, engine, OS, device type and whether it is a bot. Leave it empty to read your own browser.NetworkConvert cURL to codePaste a cURL command and get the same request as fetch, Axios, Python requests, Go or PHP. Nothing is run and nothing is sent.NetworkMarkdown previewWrite Markdown and see it rendered as you type, or copy the HTML. Raw HTML is shown as text and links are checked, so nothing in the preview can run.FormattingBarcode and QR code generatorMake a QR code, EAN-13, UPC-A, Code 128, ITF-14 or Data Matrix barcode, download it as SVG or PNG, or read a code from an image.GeneratorspagesChangelogWhat changed in each releasepagesAboutWhat codeknife.dev is and who makes itpagesIdeasSuggest improvements and vote on other visitors’ ideas.pagesPrivacyWhat leaves your browser and what is storedpagesAccessibilityConformance, known limitations and how to report a problempagesMembershipRemove ads and raise the limitspagesactions