Engineering · Open source

PHP2Shell: find every vulnerable PHP install in one command

Animated: a terminal runs sudo php2shell-check. After a short scan it lists a vulnerable PHP-FPM 8.4.10 with what to do, a patched back-ported PHP 8.3.6, a patched PHP 8.5.12, and a worker process still running a deleted PHP binary. The result line reads VULNERABLE and the exit code is 1.

PHP2Shell is a pair of critical flaws in PHP itself, tracked as CVE-2026-109618 and CVE-2026-109619. An attacker who can send a web request can run their own code on the server — no login, no bug in your application needed. The fix is simple: upgrade PHP. The hard part is knowing where all your PHP is. So we built a free tool that finds it, and we've made it open source.

Why this one is different

Most security flaws live in an application: a plugin, a framework, a form that forgot to check its input. PHP2Shell lives one layer below. It is triggered while PHP reads the body of a file-upload style request (multipart/form-data), before a single line of your code runs. Your framework, your input validation and your login page never get a say.

It affects every common way of running PHP — PHP-FPM, Apache's mod_php, php-cgi, FrankenPHP and LiteSpeed — and reaches back to at least PHP 5. It is rated CVSS 9.2, which puts it in the "patch this week, not this quarter" bracket.

PHP branchFixed inStatus
8.58.5.12Supported
8.48.4.27Supported
8.38.3.36Supported
8.28.2.35Security fixes only
8.1 and olderNo fixEnd of life — upgrade the branch

Anything below these versions, without a vendor patch, is exposed.

Why "just check the version" isn't enough

On a laptop, php -v answers the question. On real servers it rarely does:

  • There is rarely one PHP. A cPanel or Plesk server can have six side by side. Add Remi or SCL packages, a Docker image or two and a forgotten /opt/php74, and nobody has the full list.
  • Linux vendors patch without changing the number. Ubuntu, Debian and Red Hat fix the flaw in their own builds and keep the old version — 8.3.6-0ubuntu0.24.04.13 is patched, even though 8.3.6 is "below" 8.3.36. A tool that only compares numbers raises a false alarm, and teams learn to ignore alarms.
  • Patched on disk isn't patched in memory. After an upgrade, PHP-FPM workers keep running the old program until they are restarted. It is the most common mistake after patching, and nothing on disk shows it.
  • Temporary settings are per pool. The interim setting that reduces the risk, session.upload_progress.enabled=0, only helps if it is switched on in every PHP-FPM pool. One pool without it is enough.

What php2shell-check does

php2shell-check is a single program with nothing to install alongside it, for Linux, macOS, Windows and FreeBSD. You run it once and it:

  1. Finds every PHP on the machine — on the path, in distro packages, Docker official images, Remi, SCL, cPanel, Plesk, CloudLinux, LiteSpeed, Homebrew, MAMP, Herd, XAMPP, Laragon, Scoop and more.
  2. Reads the vendor's own changelog for each package, so a back-ported fix shows as patched instead of a false alarm.
  3. Looks at running processes on Linux and names any PHP worker still running a replaced or deleted program.
  4. Checks every PHP-FPM pool for the interim setting, including per-pool overrides.
  5. Says what to do for each finding — upgrade to which version, restart what — and exits with a code a CI pipeline can act on.

Each install gets one clear verdict:

VerdictWhat it means
PATCHEDAt or above the fixed release
PATCHED_BACKPORTOlder number, but the vendor's changelog lists both CVEs as fixed
VULNERABLEBelow the fixed release, with no sign of a vendor fix
VULNERABLE_EOLA branch older than 8.2 — no fix will ever ship for it
UNVERIFIEDA vendor build whose changelog couldn't be read, so it can't be called safe

Try it

On a Linux server:

curl -fsSLO https://github.com/AdZoic/php2shell-check/releases/latest/download/php2shell-check_linux_amd64
chmod +x php2shell-check_linux_amd64
sudo ./php2shell-check_linux_amd64   # root sees every worker and pool

For container images, from any machine with Docker or Podman:

php2shell-check image php:8.3-fpm my-registry/app:prod

It exits with 0 when everything is safe, 1 when something is vulnerable, 2 when something needs a person to look, and 3 when a scan couldn't run — so it can stop a deployment on its own. For GitHub, it is also a one-line Action:

- uses: AdZoic/php2shell-check@v1
  with:
    images: my-registry/app:latest

Builds for every platform, with checksums, are on the releases page.

Safe by design

php2shell-check is a defensive tool. It only asks each PHP for its version and settings, and asks the package manager what it installed. It never runs PHP code, never sends a request to your websites and contains no attack code. Image scans run in a throwaway container with no network, no special permissions and a read-only filesystem, and the container is always removed afterwards. Anything it can't prove is patched is reported as needing attention — never as safe.

What to do today

  1. Run it on every server, and in CI against every image you ship.
  2. Upgrade anything marked VULNERABLE to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 or later — or install your Linux vendor's patched package.
  3. Restart PHP-FPM and the web server, then run it again to confirm no old workers are left.
  4. Plan the move off anything marked VULNERABLE_EOL. Interim settings reduce the risk; only upgrading removes it.

Why we built it

We run PHP ourselves, and the first question after the advisory was the obvious one: where, exactly? Answering it by hand across servers, panels and images was slow and easy to get wrong — and the tools we tried either cried wolf on patched Ubuntu builds or said nothing about workers still running old code. A scanner that is honest about what it can and can't prove seemed worth sharing. It is free and MIT-licensed, and pull requests are welcome, especially for PHP install locations it doesn't know yet.

A note on sources

Affected versions, fixed releases and the interim setting follow the PHP project's advisory for CVE-2026-109618 and CVE-2026-109619. How the tool behaves is described from its own source code and test suite, both public on GitHub. This post is general guidance, not advice for your specific setup — check your vendor's advisory for the exact package that fixes your system.

This post also appears on our developer blog, adzoic.github.io, where we write about the open-source tools we publish.

A
AdZoic team
Making ad buying make sense, from Dhaka.
Keep reading

Related reading

Animated: ad requests stream through ingress, Go bidder and cache-plus-models layers into a bid decision inside the auction deadlineEngineering

Inside adZoic: the architecture behind millions of ad decisions a day

Our stack, system design, security posture and scaling approach — shared openly, with a diagram and honest numbers.

Aug 2026 · 8 min read
Animated: five retries travel as one tight wave and overload a dependency; with jitter they arrive one at a time and the dependency stays calmEngineering

The jitter that never jittered: how a passing test hid a real bug

Our retry backoff had jitter, it ran on every publish, and it returned the same number every single time. The test passed. A postmortem.

Sep 2026 · 8 min read
Animated: one copy of an event stream reserves far more of a machine than it uses; measured and resized, three copies fit on three machines with none boughtEngineering

Measure first, buy last: how we keep our infrastructure lean

The whole platform is code, one definition serves every environment, and most of our best fixes were a measurement rather than a purchase.

Sep 2026 · 12 min read

← Back to all posts