Does this sound familiar, or at least possible? You install a third-party dependency for a local project via a standard package installation. Maybe it's for a project that has zero PHP. You aren't in a panic about the possibility of a malicious sub-dependency at the moment because you know it cannot access your sensitive local files; your application isolation limits the scope of what any dependency can do.
You definitely aren't assuming such a dependency could execute shell command with your user privileges.
However, a quick sweep of the local network reveals an unpatched PHP process bound to the loopback interface (127.0.0.1) running. Get ready to be fully pwnd.
That limited-access sub-dependency you weren't too worried about only has to send a specifically-crafted payload targeting your running 127.0.0.1 endpoint to trigger PHP2Shell, a core PHP engine memory flaw. The malware has now escaped the restricted sandbox it was in and can now run native shell commands under your primary user account, exposing, for example, your SSH keys, env files, API tokens, and source code repositories.
Note that this threat did not originate from an external network access. It leveraged a possibly forgotten local development binary as a privilege-escalation ramp.
This scenario highlights the risk—even on local environments—from PHP2Shell (tracked under Common Vulnerabilities and Exposures entries CVE-2026-109618 and CVE-2026-109619), a set of critical use-after-free Remote Code Execution (RCE) vulnerabilities residing within the PHP core engine.
The Risk to Local Development Workstations
Developers frequently assume that software running exclusively on 127.0.0.1 is immune to external attacks because routers block incoming traffic to the loopback interface.
That boundary disappears when malicious code is already executing locally on the host machine—whether via a compromised package manager dependency, a malicious browser extension, or an untrusted binary. If a low-privilege process can hit a local port, vulnerabilities like PHP2Shell act as a privilege-escalation vector, allowing compromised sandboxed contexts to be promoted into full user execution.
Step 1: Audit All Installed PHP Versions
Have I convinced you that you should upgrade to a patched version of php on your local machine?
Good. But it gets worse for those of us maintaining local development boxes: updating only the primary system PHP binary is only a partial fix if you leave secondary, version-pinned binaries unpatched across your various local projects.
macOS does not include a production-ready PHP environment by default, but developers often maintain multiple concurrent versions using Homebrew formulas (php@8.2, php@8.3, php@8.4), version managers (such as phpenv or asdf), or isolation tools like Laravel Herd, MAMP, and Docker.
So you will need to run a comprehensive search across your system paths to locate all active PHP executables:
# Check primary PATH resolution which -a php # List all version-pinned Homebrew installations brew list --versions | grep php # Audit standard binary directories for hidden or pinned versions ls -l /opt/homebrew/opt/php*/bin/php /usr/local/opt/php*/bin/php 2>/dev/null Inspect each discovered executable to check its patch level:
/opt/homebrew/opt/php@8.3/bin/php -v /opt/homebrew/opt/php@8.4/bin/php -v Step 2: Apply the Patch Across All Installed Versions
The vulnerabilities affect all PHP releases prior to the emergency security patches. You must ensure every installed version branch on your system meets or exceeds the safe release thresholds:
- PHP 8.2: Safe at 8.2.35 or higher
- PHP 8.3: Safe at 8.3.36 or higher
- PHP 8.4: Safe at 8.4.27 or higher
- PHP 8.5: Safe at 8.5.12 or higher
(Note: PHP 8.1 and older branches are end-of-life and do not receive official upstream patches from php.net.)
Patching via Homebrew
Updating the default php formula alone will not patch version-pinned formulas. You must upgrade all installed PHP formulas explicitly:
brew update # Upgrade all Homebrew formulas simultaneously brew upgrade # Or target specific pinned formulas individually brew upgrade php php@8.2 php@8.3 php@8.4 Patching Environment Managers & Local Stacks
- Laravel Herd / MAMP / DDEV: Open the respective application's settings or version management UI, refresh the version feeds, and install the updated point releases for every PHP version installed within the application.
-
phpenvorasdf: Re-build or re-install the latest patch versions using your respective plugin utilities (e.g.,asdf install php 8.3.36). - Docker / Containerized Local Envs: Update base image tags in your
Dockerfileordocker-compose.ymlto point to the patched point release image tags and rebuild your dev containers (docker compose build --no-cache).
Dealing with Legacy Cruft: Old PHP 5, 7, and 8.1 Projects
Most developers have an archive folder filled with older sites running PHP 8.1, PHP 7.x, or even legacy PHP 5 codebases. Because older branches are long past their end-of-life dates, official security patches do not exist for them. Going through every old codebase to refactor syntax for modern PHP versions is a massive time sink.
However, leaving unpatched, abandoned runtimes sitting quietly in your workspace creates a persistent risk window. To neutralize old projects without upgrading their codebases, apply these containment strategies:
- Purge Unused Binaries: If you no longer actively spin up a specific legacy PHP version for daily work, uninstall the binary package entirely from Homebrew or your environment manager. Having the code files sit on your disk is completely harmless unless an interpreter is installed to execute them.
- Quarantine Archive Folders: Move legacy projects out of your active development path and into an isolated directory (e.g.,
~/Archive/Legacy-PHP/). Ensure your local development managers (like Valet or Herd) are not configured to automatically serve or scan that directory. - Containerize If Execution Is Required: If you must occasionally spin up an ancient project for reference, run it strictly inside an isolated Docker container with zero host-volume bindings to sensitive directories, and shut the container down immediately after use.
Auditing every pinned instance and locking away unmaintained legacy code guarantees that forgotten background processes do not remain open as exposure windows.
Cover photo by Benjamin Olivier Schaeuffele
