On October 21, 2026, PHP shipped emergency fixes for two critical flaws in its core runtime, nicknamed PHP2Shell, that allow pre-authentication remote code execution. The release was moved up a day after details leaked, and Amazon later said it had seen an exploit attempt against one of its honeypots four days before the planned disclosure. The Hacker News has covered it in several articles.
This one is different from most incidents in this series. The bug isn't in anyone's application code. It's in the interpreter that runs it, which changes what helps and what doesn't.
What happened
According to The Hacker News' reporting:
- The two flaws are CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2 by PHP. A leaked post had claimed 10.0; that was not PHP's rating.
- They were found by Wiz Research, Calif and OpenAI's Codex Security team, who worked with the PHP security team and Google's Gemini team on the fix.
- CVE-2026-109618 is described as an out-of-memory flaw. CVE-2026-109619 involves session upload progress, which can reach session handling before PHP finishes setting up a request. The articles don't describe the mechanisms in more detail.
- Fixed releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Branches older than 8.2 no longer get project releases; the PHP security team published source backports for 7.4, 8.0 and 8.1. Reports say nearly every version back to 5.4 is affected.
- Exposed server setups named in the reporting are PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. mod_php is described as the highest risk: a demonstrated exploit needs a single unauthenticated request and fires before application code runs. For CVE-2026-109619, code execution was reached on both mod_php and PHP-FPM.
- As CrowdStrike put it: "There is no authentication to bypass, no session to steal, and no vulnerable endpoint to discover." The exposure is independent of the CMS, framework or plug-ins in use.
The disclosure did not go to plan. A leak on October 20 said disclosure was set for October 22; Calif mistakenly posted a proof-of-concept video around 20:00 UTC that day; Chainguard published patched images at 07:41 UTC on October 21, before the official fix; and PHP moved its release up to October 21. Amazon said its MadPot honeypot recorded an exploit attempt dated October 17, from an anonymized IP address, while the flaws were still under embargo.
After the release, exploitation attempts followed quickly. watchTowr's update reported "active exploitation attempts" of both CVEs, Pantheon said it had blocked over 60,000 suspicious requests by the evening of October 21, and VulnCheck saw hosts scanning with the user-agent php2shell-scan/109618. At the time of reporting no source had confirmed a successful break-in of a specific server, and neither flaw was in CISA's known-exploited catalog.
The scale is the sobering part: VulnCheck counted PHP on about 2.6 million internet-facing hosts, and Modat counted 4.38 million PHP services, with about 79% on unsupported branches.
Why it worked
Most of what we write about is a mistake in code you own: a string-built query, a shell command, a missing ownership check. Those can be found by reading your code. PHP2Shell is a flaw in the runtime underneath, triggered while PHP sets up a request, before any of your code runs. Your application can be flawless and still be exposed.
Two other things made it worse.
Runtimes are invisible inventory. Updating WordPress or Laravel doesn't update the PHP binary under it. On shared hosting, in an old base image, or on a VM nobody owns, the PHP version is whatever was installed years ago. Sonatype's advice in the coverage was the right first question: know where PHP is used, who owns remediation, and how fast fixes can deploy.
Version strings lie in both directions. Debian and Ubuntu backport fixes, so a patched package can keep an old-looking version number. And a version banner in an HTTP header tells you what a server says it runs, not whether it carries the backport.
Why stopgaps weren't enough
Before the fix shipped, several workarounds circulated. The reporting is consistent that none of them is a full answer:
- Disabling session upload progress (
session.upload_progress.enabled=0) blocks only the confirmed session paths for CVE-2026-109619 and does nothing for CVE-2026-109618. PHP 8.6's release manager, Joe Ferguson, put it bluntly: "ini settings won't save you." - WAF rules only see traffic that passes through them. Cloudflare's CTO called its emergency rules "a STOPGAP only," and Cloudflare reported attackers changing payloads to get around them.
- Blocking multipart bodies at the web server reduces exposure but breaks uploads, and isn't a guarantee.
The advisory's own words: "Only updating is a complete fix."
If you can't update immediately, the advisory's interim settings, as reported, look like this. Verify them on the process that serves web traffic (PHP-FPM or mod_php), not with php -i in a shell, which reads a different configuration:
; Interim hardening only. Updating PHP is the fix.
session.upload_progress.enabled = 0 ; then restart all workers
post_max_size = 8M ; keep request bodies small
; PHP 8.5 only:
fatal_error_backtraces = 0The advisory also asks for memory_limit=-1 so applications can't override it, request-size limits at the web server or proxy, and warns against using RLIMIT_AS or RLIMIT_DATA, which can trigger the same vulnerable path.
What to do now
- Find every PHP runtime you run. Servers, containers, base images, shared hosting, forgotten VMs. The application inventory is not enough; you need the interpreter inventory.
- Update PHP to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 or later. For 8.1 and older, upgrade the branch, apply a vendor or distribution backport, or take the system offline or isolate it.
- Check distribution packages by CVE, not version string. For example, on Debian or Ubuntu:
apt changelog php8.3-fpm | grep CVE-2026-109618. - Rebuild images. Pin your PHP base image to a fixed release, and to a digest once you've verified it, so a rebuild can't silently pull an old one:
# Illustrative: pin the runtime you verified, not a moving tag
FROM php:8.3.36-fpm@sha256:<digest-you-verified>- Restart every worker after updating packages or changing ini settings; a running FPM pool keeps the old binary and configuration.
- Isolate what you can't patch yet. CERT-EU advised considering isolating internet-facing applications from public networks until a fix is applied: a VPN, allowlisted clients or authenticated access at the edge, and confirm the origin can't be reached directly around those controls.
- Limit what a compromise would yield. Remove stored credentials and over-broad cloud roles from PHP hosts, keep logs off the box, and review logs from before you patched. The FBI's advice in the coverage was to watch for "PHP processes unexpectedly launching command shells or downloading files." CrowdStrike noted why logs alone may not show it: "Worker processes remain stable, responses return HTTP 200, and application logs record no anomalies."
- Beware fake exploits. JFrog reported fake proofs of concept and prank malware on GitHub. Don't run untrusted exploit code to "test" your servers.
Turning off the version banner (expose_php = Off in php.ini) is reasonable hygiene, but it is not a defence here: mass scanners like php2shell-scan don't need a banner to try.
How OpenRouting helps
To be direct: no code scan, ours included, would have found PHP2Shell, because the flaw isn't in your code. OpenRouting doesn't detect it. What it can help with is the inventory and the blast radius.
- [Passive website scans](/use-cases/dynamic-scanning) run OWASP ZAP's passive rules against your sites, which flag servers that leak version information through headers such as
X-Powered-By: PHP/…andServer. That's a quick way to see which of your public sites announce PHP, and which banner versions are clearly old. Passive means no attack payloads are sent. - Repository scans run Semgrep's IaC rules over your Dockerfiles and compose files, and the container and Kubernetes agent reviews those findings for hardening such as a non-root user, dropped capabilities and a read-only filesystem. That doesn't stop a runtime RCE, but it limits what code running as the PHP process can do next. Pinning the base image is still on you; our post on Dockerfile security mistakes covers it.
- Semgrep's secret rules flag credentials committed in the files you scan, such as a database password in a PHP config file, and the blast radius agent reviews how far one compromised host could reach. Both shrink what a runtime RCE hands an attacker. Secret values are redacted before any AI call.
- Semgrep's PHP rules still matter for the bugs that *are* in your code, such as
unserializeon request data or user input reachinginclude. AI triage, by Claude, via Amazon Bedrock, gives each finding a verdict, a plain-English explanation and a suggested fix.
What it won't catch
- The flaw itself. PHP2Shell lives in the runtime. A code scan reads your source, and the passive website scan sends no exploit payloads, so neither tests whether a server is vulnerable.
- Your PHP version. OpenRouting doesn't do dependency or runtime-version (SCA) scanning yet. A version banner, when present, is a hint, not proof: backported packages keep old version strings, and many servers hide the banner.
- Servers without a public site. Passive scans only see sites you add as targets; internal or VPN-only hosts aren't reachable.
- Detection of an attack in progress. OpenRouting isn't a runtime monitor. Use process monitoring and your logs for that.
- Findings need a scan. Results appear when you run a scan from the dashboard or review a pull request.
Sources
- PHP issues emergency patches for two critical PHP2Shell RCE flaws as exploit attempts emerge (The Hacker News)
- Someone tried to exploit PHP2Shell 4 days before the vulnerabilities were publicly disclosed (The Hacker News)
- PHP2Shell: what actually protects you before the PHP RCE patch, and what doesn't (The Hacker News)
- PHP2Shell: two unpatched critical PHP RCE flaws trigger emergency security alert (The Hacker News)