Code Security

Patched PHP2Shell? Here’s What Most Teams Still Miss (CVE-2026-109618 & 109619)

Amartya | CodeAnt AI Code Review Platform
Sonali Sood

Founding GTM, CodeAnt AI

Running a single apt upgrade on your main web server is the part of PHP2Shell remediation that feels like progress and delivers the least assurance. The two unauthenticated remote code execution flaws in PHP core, CVE-2026-109618 and CVE-2026-109619 (CVSS v4.0 9.2 each), were being exploited before public disclosure, and the memory-disclosure side effect means a patched server can still be a breached one. Patching is step three of five, not step one of one.

This is the operational runbook, written for the people who own the servers. If you want the code-level breakdown of how the bug works, that is a separate deep dive. This piece assumes you already accept that you must update PHP, and answers the harder question: how do you prove you actually fixed it everywhere, and what do you owe your users after an exposure window that may predate your patch?

Attribution note. CodeAnt did not discover PHP2Shell and has not independently reproduced it. This playbook is built on the public guidance from the PHP project, Calif, Wiz, Sansec, AWS and others. Everything below is remediation procedure; nothing here is claimed as a CodeAnt finding.

The Five-Step PHP2Shell Response, At a Glance

Step

Goal

The question it answers

1. Inventory

Find every PHP install and version

"Where is PHP actually running, including the boxes I forgot?"

2. Verify

Confirm the patch took in the serving context

"Is the worker handling requests really on the fixed build?"

3. Handle EOL

Deal with unsupported PHP that gets no fix

"What do I do about PHP 8.1 and older?"

4. Assume disclosure

Rotate what the memory leak could have exposed

"What secrets do I treat as burned?"

5. Build standing detection

Make the next PHP core bug a non-event

"How do I not scramble like this again?"

Work them in order. Skipping to step 2 before you have finished step 1 is how a forgotten staging box becomes the foothold.

Step 1: Inventory: find every PHP version before you trust any of them

The single most common failure in a PHP emergency is remediating the servers you remember and missing the ones you don't. A realistic estate has PHP in places that never make it onto a diagram: a legacy reporting box, a vendor appliance, a cron-only worker, an old container image still being pulled by one deployment, a developer's forgotten VM with a public IP.

Your goal in this step is a complete list of every host running PHP, and the exact version each one serves.

How to check the PHP version on a server

The version you care about is the one that serves HTTP, which is not always the one on the command line. Check both, per host:

  • Command line (the CLI SAPI): php -v. This tells you the CLI version and nothing authoritative about what your web traffic hits.

  • The web-facing SAPI: render a phpinfo() page behind authentication, or read the version your application exposes, and confirm it against the CLI result. If they differ, the web version is the one that matters for this CVE.

  • Per installed branch: systems with multiple PHP versions installed (common on cPanel and multi-site hosts) answer php74 -v, php82 -v, php83 -v and so on separately. One patched binary does not mean the others are.

How to check the PHP version a website is running

For assets you only know by URL, the running version often leaks in the X-Powered-By response header (unless suppressed) or in server-status pages. Treat this as reconnaissance of your own estate: if you can read the version from outside, so can an attacker deciding whether you are worth a payload.

Don't forget the usual blind spots

  • WordPress, Drupal, Joomla and Magento hosts: the admin dashboard usually reports the PHP version (for WordPress, Tools → Site Health → Info → Server). CMS fleets are where orphaned, out-of-date PHP tends to accumulate.

  • Containers and images: the version baked into an image is not the version until the image is rebuilt and redeployed. Inventory your registry, not just your running hosts.

  • Managed and serverless PHP: platform runtimes (Elastic Beanstalk, App Engine, managed hosting) patch on their own schedule; confirm your platform version against the provider's bulletin rather than assuming.

Only once you have the full list, every host, every SAPI, every container image, every EOL box, do you move on. The list is the deliverable of step 1.

Step 2: Verify the patch actually took where requests are served

This is the step most teams think they can skip, and it is the one that most often hides a still-vulnerable worker. Updating the package is not the same as the live worker running the new code. Three traps account for nearly every "we patched but were still hit" story.

Trap 1: PHP-FPM is still running the old workers

PHP-FPM loads its configuration and binary into long-lived worker processes. Upgrading the package on disk does not swap the code in a running master; the workers keep serving the old build until you reload or restart the service.

After upgrading, reload or restart FPM explicitly for every pool and every version:

# per installed version; substitute your version
systemctl restart php8.3-fpm
# per installed version; substitute your version
systemctl restart php8.3-fpm
# per installed version; substitute your version
systemctl restart php8.3-fpm

Then confirm the serving version from a request actually handled by FPM (your phpinfo() page or application version endpoint), not from php -v on the shell. PHP-FPM reads its own pool configuration, not the CLI php.ini, so a command-line check can report "fixed" while your site is not.

Trap 2: the container is still the old image

A running container keeps the PHP it was built with. Pulling a fixed base image changes nothing until you rebuild and redeploy the containers that depend on it. Verify the version inside a running container (docker exec <container> php -v and the FPM-served version), and confirm your orchestrator actually rolled the new image rather than keeping the old one warm.

Trap 3: the wrong binary got patched

On multi-version hosts, confirm that the binary your web server is wired to is the one you updated. A patched php82 does nothing for a vhost still routed to php81-fpm.

A clean verification is: for each entry on your step-1 list, the web-served version string reads a fixed release (8.5.12, 8.4.27, 8.3.36 or 8.2.35 or later), confirmed from a request path, after the workers were reloaded.

Step 3: The end-of-life trap, what to do about unsupported PHP

A large share of the internet runs PHP that the project no longer supports, and those versions are the sharpest edge of this incident: PHP 8.1 and earlier are end of life and receive no official fix for PHP2Shell. The vulnerable code reaches back to PHP 5.4.0, so an unsupported host is both affected and unpatchable through normal channels.

Reference points for the versions you are likely to find:

PHP branch

Status for PHP2Shell

PHP 8.5

Fixed in 8.5.12

PHP 8.4

Fixed in 8.4.27

PHP 8.3

Fixed in 8.3.36

PHP 8.2

Fixed in 8.2.35

PHP 8.1

End of life — no official fix

PHP 8.0, 7.4 and earlier

End of life — no official fix

If step 1 turned up end-of-life PHP, you have three honest options, in order of preference:

  1. Migrate to a supported, fixed branch now. This is the only path that actually closes the hole. Treat the incident as the forcing function for an upgrade you were probably already overdue on.

  2. Get a vendor backport. Some distributions and managed platforms backport fixes to versions upstream has retired. Confirm the specific CVE is addressed, not just that "an update is available."

  3. Isolate and compensate as a stopgap. If you truly cannot move a box today, reduce its reachability (take it off the public internet, restrict it to known clients) while you plan migration. Treat this as triage with a deadline, not a resting state. Temporary edge mitigations exist but are partial and belong in a short holding window only.

One operational note that bites during EOL cleanup: if you install PHP from a third-party community repository rather than your distribution's packages, the fix may lag the official release by days. Verify your source actually shipped the patched version before you mark a host green.

Step 4: Assume disclosure: the credential-rotation checklist

Here is the part almost every "patch now" article omits, and the reason patching alone is not closure. One of the two bugs, per Sansec's analysis, can return freed PHP worker memory to an unauthenticated client. Because it runs before application code, the leak can happen even on endpoints that never start a session, and the leaked bytes can contain secrets that passed through that worker: session tokens, API keys, and in the worst case TLS private keys.

The practical consequence: if a host was internet-reachable during its exposure window, treat the secrets that lived in its PHP workers as potentially disclosed. Exploitation attempts were observed before the public announcement, so "we patched quickly" does not rule out a quiet earlier probe.

Your exposure window is roughly: from whenever your affected build went public-facing, until the moment you completed step 2 on that host. Secrets used by that host during that window go on the rotation list.

PHP incident response checklist: what to rotate, in what order

Prioritize by blast radius:

  1. TLS private keys on affected, internet-facing hosts. Reissue certificates and revoke the old ones. This is the highest-impact item because a leaked key compromises confidentiality of traffic, not just one account.

  2. Application and framework secrets: the app key / session-encryption secret (for example Laravel's APP_KEY, WordPress salts), which protect every session and signed token. Rotate and force re-authentication.

  3. Outbound API keys and tokens held by the application: payment processors, mail providers, cloud and third-party service credentials. Anything the PHP process could read from environment or config.

  4. Database and service credentials the worker used, followed by a review for unexpected access using the old ones.

  5. Active user sessions: invalidate them so any token that leaked can no longer be replayed.

Pair rotation with a look back: review logs over the exposure window for the request shapes associated with this vulnerability and for anomalous access using now-rotated credentials. (The specific indicators of PHP2Shell exploitation traffic are covered in the technical deep dive; here they are an input to your log review, not a signature to deploy and forget.)

This step is uncomfortable because it converts "we patched" into "we had an exposure window and must act as if something got out." That discomfort is the correct posture for a pre-disclosure-exploited, memory-leaking RCE.

Step 5: Build standing detection so the next PHP core bug is a non-event

PHP2Shell will not be the last critical bug in a runtime you depend on. The teams that handled this calmly are the ones who could answer "where is PHP and what version?" in minutes, not days. Turn this incident into that capability.

Continuous asset and version inventory

The thing that made step 1 painful was not having a live inventory. Maintain one: every host and container, the PHP version each serves, and its support status, updated automatically, not reconstructed under pressure. When the next advisory drops, triage should be a query, not an archaeology project.

Version-drift and end-of-life alerting

Alert when any asset drifts onto an unsupported PHP branch before a CVE forces the issue. End-of-life PHP is a standing liability; monitoring for it converts a future emergency into a routine upgrade ticket.

Behavioral detection at the edge

Because engine-level flaws execute before application code, application logging sees them late or not at all. Detection belongs at the edge and in the runtime's own crash signals: correlate the request shapes associated with the exploit with worker crashes and out-of-memory kills. Signature matching alone is weak for memory-pressure bugs; behavioral and anomaly-based detection ages better.

Posture that assumes the dependency, not just your code, is the risk

PHP2Shell is a reminder that your most exposed attack surface can be a dependency you never wrote and rarely think about. A security posture that only reviews first-party code will miss it entirely. Continuous visibility into the runtimes and dependencies you ship, and automated code review that reasons about error-handling and interruption paths rather than just known-bad patterns, is what shortens the gap between "a core CVE dropped" and "we are fully remediated and can prove it." That gap is the whole ballgame once a public exploit exists.

Bottom Line

Patching PHP stops the bleeding; it does not close the case. Inventory everything, verify the fix in the context that serves requests, deal honestly with end-of-life versions, rotate what the memory leak could have exposed, and stand up the inventory and detection that make the next runtime CVE boring. The organizations that come through PHP2Shell well will be the ones who treated "update PHP" as the middle of the work, not the end of it.

Sources & Further Reading

  • PHP project, October 2026 security release (CVE-2026-109618, CVE-2026-109619)

  • Sansec Forensics, PHP2Shell analysis (memory-disclosure behavior)

  • AWS Security Bulletin 2026-143 (PHP web runtimes; managed-platform guidance)

  • Tenable and JFrog advisories on CVE-2026-109618 / CVE-2026-109619

  • PHP supported versions and end-of-life schedule (php.net)

FAQs

How do I check which PHP version my website is running?

I updated the PHP package but is my site actually patched?

Is PHP 8.1 safe from PHP2Shell?

Do I need to rotate credentials after patching PHP?

How do I find every PHP install across my infrastructure?

Start Your 14-Day Free Trial

AI code reviews, security and quality trusted by modern engineering teams.

Table of Content
No headings found on page

Ship clean & secure code faster

Get Pentest Report

NO CC REQUIRED