Patch the CVE, Don't Trigger an Accidental Refactor: Surviving PHP2Shell on Docker
When national cyber agencies like the UK NCSC issue emergency threat alerts for a critical CVSS 9.2 unauthenticated Remote Code Execution in PHP core (PHP2Shell / CVE-2026-109618 & CVE-2026-109619), production Slack channels lose their minds.
The immediate knee-jerk reaction from most developers and ops teams is always identical:
"Drop everything. Upgrade all containers to PHP 8.5 right now."
Don't do that.
Rushing a major runtime upgrade during an active vulnerability drill is how you cause the exact production downtime the attacker was hoping to achieve. You take down custom WordPress themes, break WooCommerce extensions, trigger fatal deprecation errors across payment gateways, and suddenly spend your night debugging runtime incompatibilities instead of patching a security hole.
Here is the SRE incident playbook on how point patches work, why your Docker layer caching is probably lying to you, and the deterministic way to patch containerized PHP stacks without breaking production.
1. Upstream Did the Heavy Lifting for You
Security releases do not force you across major version boundaries. The PHP maintainers backported fixes for both the out-of-memory memory corruption and the worker cross-request use-after-free bugs directly into active branches:
- PHP 8.2 →
8.2.35 - PHP 8.3 →
8.3.36 - PHP 8.4 →
8.4.27 - PHP 8.5 →
8.5.12
(Note: If you are running PHP 8.1 or older, you are already past End-of-Life and on your own. You have no upstream patches and need extended vendor security repos or a scheduled upgrade window.)
If your app is running on PHP 8.2 or 8.3, you do not need to move to 8.4 or 8.5 today. You only need to step to the patched point release on your existing branch. Zero syntax breaking changes, zero extension incompatibilities.
2. The Docker Tag Trap: Why "Rebuilding" Didn't Fix It
Most engineers maintain Dockerfiles that look something like this:
FROM php:8.3-fpm-alpine
When news of the patch breaks, they run:
docker compose build
docker compose up -d
They think they are safe. They are not.
Docker tags are mutable aliases, but the Docker daemon’s caching engine is aggressively lazy:
- Docker checks your local image cache.
- It sees an image already tagged
php:8.3-fpm-alpinefrom three weeks ago. - It assumes nothing has changed and builds your application container using the old, vulnerable base layer.
Unless you explicitly tell Docker to check the registry for a new manifest digest, it will happily serve cached, vulnerable binaries forever.
3. The Immutable Alternative: Pin Exact Subversions
Floating tags (php:8.3-fpm) are convenient for local sandbox playgrounds, but they hide state in production CI/CD pipelines.
If you pin your Dockerfile to the exact patched point release, you introduce deterministic traceability into Git version control:
# Lock to the patched upstream release
FROM php:8.3.36-fpm-alpine
Now the Git diff explicitly documents that the security vulnerability was evaluated, addressed, and tracked.
4. The 3-Step Clean Rebuild Playbook
Whether you pin exact subversions or use floating branch tags, you must force Docker to pull upstream manifests and ignore local cached base layers.
Step 1: Force Upstream Pull and Bypass Cache
Run your build with --pull (forces registry check for new tag digests) and --no-cache:
docker compose build --pull --no-cache php
Step 2: Force Recreate the Service Container
Prevent Docker from reusing an existing container instance:
docker compose up -d --force-recreate php
Step 3: Verify the Running Runtime
Never trust command output—trust the running process memory. Execute directly inside the active container:
docker compose exec php php -v
Look at the output:
PHP 8.3.36 (cli) ...→ Patched.PHP 8.3.35or lower → Still vulnerable. Investigate your registry pulls immediately.
5. Stopgap Protections While You Roll Out
If you operate multiple environments or orchestrate fleets that take time to redeploy, lock down the vector at your ingress layer:
- WAF Body Inspection: Ensure your edge proxy (Cloudflare, AWS WAF) is active. Cloudflare has rolled out managed rule mitigations across all tiers targeting multipart/OOM fuzzing payloads.
- Cycle PHP-FPM Workers Aggressively: In your pool configuration (
www.conf), clamppm.max_requests = 100. This destroys and respawns workers frequently, sharply limiting stale memory persistence across requests. - Limit POST Body Sizes at the Reverse Proxy: Clamp
client_max_body_sizein Nginx/Traefik down to what your app actually requires so large payload floods never reach PHP workers in the first place.
Key Takeaway
A zero-day or critical CVE drill is not the time to modernize your runtime architecture.
Stay on your active minor branch, pull the exact upstream point release, force Docker to bust its layer cache, and verify the running process.
Patch the vulnerability. Don't trigger an accidental refactor.
Comments
4 Common Routing Traps When Migrating to Nuxt i18n
How to fix broken navigation, missing widgets, and active links when migrating a NuxtJS app to @nuxtjs/i18n.
pfSense basic firewall setup
pfSense basic firewall setup
pfSense basic firewall setup - Dive into the details of pfsense basic firewall setup with this quick guide.
