DEV Community

CodeMonkeyG
CodeMonkeyG

Posted on Originally published at codemonkeyg.com on

Patching a Critical PHP Runtime Vulnerability Across Seven Environments

Publication note. This case study has been deliberately anonymized. Organization names, domains, repository names, container names, network details, credentials, and infrastructure-specific paths have been removed or generalized. The version thresholds, remediation approach, validation steps, and operational obstacles are retained because they are useful without exposing the affected environments.

Executive summary

A newly disclosed pair of critical PHP runtime vulnerabilities required a rapid review of several containerized web applications. The important question was not whether a repository declared support for a particular PHP major version. The question was: what PHP version is actually executing public requests right now?

The review found seven public-facing PHP services built from mutable container tags. Source files did not prove that any live service was vulnerable, but they did show that the deployed patch level could drift whenever an image was rebuilt. Runtime verification confirmed that all seven active services were below their applicable security fix threshold.

The remediation did not require compiling PHP from source. Each application container was rebuilt from an official, fixed PHP base image pinned both to an exact release tag and an immutable image digest. The services were then recreated one at a time, leaving proxies, databases, and unrelated application services running. Final checks inside every active PHP worker confirmed the fixed versions.

Two builds initially failed because Docker could not read root-owned runtime directories that had been included in their build contexts. Narrow .dockerignore rules excluded only those non-build artifacts. After rebuilding, all seven services ran fixed PHP versions.

1. Start with deployed reality, not framework requirements

The affected applications used a mix of PHP-FPM-backed frameworks and a content-management workload. Their dependency manifests expressed broad compatibility ranges such as PHP 8.3 or later. That is useful for application development, but it does not prove that the operating container has received a particular runtime security release.

The same distinction applies to a Dockerfile line such as:

FROM php:8.3-fpm
Enter fullscreen mode Exit fullscreen mode

That tag is convenient, but mutable. A build performed at one time may produce a different image than a build performed later, and a running container may still be based on an older cached image. The corresponding risk was even less visible where a content-management image used a generic, unversioned runtime tag.

The initial static review established three facts:

  1. Each affected service accepted PHP requests through a public web path.
  2. Each runtime image tag was mutable or did not identify a PHP patch release.
  3. A source-only review could classify exposure as possible, but could not establish the live runtime version.

That last boundary mattered. Treating an image reference as proof of the running runtime would have been an assumption, not a finding.

2. Define the version policy before deployment

The disclosed issues were fixed in specific PHP patch releases. The response policy treated these versions as the minimum acceptable upstream releases:

PHP branch Minimum fixed version
8.2 8.2.35
8.3 8.3.36
8.4 8.4.27
8.5 8.5.12

An environment running an older distribution-maintained PHP package may have a vendor backport. In that case, the package maintainer's advisory and changelog must establish that the fix is present. This fleet used official container images, so the operational rule was simpler: move each service to the published fixed patch release or later.

We first queried the executing process in every active PHP container:

docker exec <php-container> php -r 'echo PHP_VERSION, PHP_EOL;'
Enter fullscreen mode Exit fullscreen mode

All seven returned versions below their branch threshold. This converted a potential exposure based on source review into a confirmed patching requirement.

3. Pin both the release and the image identity

The permanent source change was intentionally small. Each generic PHP image tag was replaced by an exact fixed patch release plus its immutable registry digest:

FROM php:8.3.36-fpm@sha256:<verified-image-digest>
Enter fullscreen mode Exit fullscreen mode

Services on the next supported branch used the equivalent fixed release:

FROM php:8.4.27-fpm@sha256:<verified-image-digest>
Enter fullscreen mode Exit fullscreen mode

The content-management service and its associated command-line image received the same treatment: an explicit image variant with a verified fixed PHP runtime and a pinned digest.

Tags make source files readable and state the intended release. Digests bind that declaration to the exact multi-platform image manifest that was inspected. Together they prevent a future rebuild from silently receiving a different image under the same tag.

Before changing application source, the candidate images were pulled and inspected locally. The PHP executable inside each image was queried directly:

docker run --rm --entrypoint php <image-reference> -v
Enter fullscreen mode Exit fullscreen mode

This validated the runtime carried by the selected image rather than trusting the tag name alone.

4. Rebuild application containers, not PHP from source

There are two very different meanings of "rebuild PHP":

Action What it means What we did
Compile PHP from source Download PHP source, compile it, and manage build flags and patches No
Rebuild an application image Run the application's Docker build again, using an updated PHP base image Yes

The latter was the correct response for this fleet. It preserved each application's existing extension installation and startup behavior while replacing its underlying, vulnerable PHP runtime with the fixed image.

The normal deployment sequence was:

docker compose build --pull <php-service>
docker compose up -d --no-deps --force-recreate <php-service>
Enter fullscreen mode Exit fullscreen mode

--pull makes the build retrieve the declared digest if it is not already present. --no-deps limits the restart to the patched service. --force-recreate ensures that Compose does not keep an existing, vulnerable container merely because its service configuration appears otherwise unchanged.

This was performed one service at a time. Reverse proxies, databases, caches, and front-end services were not rebuilt or restarted as part of this runtime-only response. That reduced the change surface and made troubleshooting clearer.

5. Roadblock: runtime files leaked into the build context

The first four application image builds completed. Two later builds failed before executing their Dockerfiles because the Docker daemon could not stat directories inside the build contexts. Those directories were root-owned runtime artifacts under application storage paths.

The failure was not a PHP compatibility issue. It came from Docker sending the entire project directory as build context. A root-owned directory that is useful at runtime does not need to be sent to the builder, especially when the Dockerfile does not copy it into the image.

The fix was a narrow .dockerignore entry in each affected repository:

storage/framework/testing/disks/public/<runtime-directory>/
Enter fullscreen mode Exit fullscreen mode

and:

application/storage/app/private/<runtime-directory>/
Enter fullscreen mode Exit fullscreen mode

These exclusions were deliberately specific. We did not delete runtime data, change ownership recursively, or ignore broad application storage trees. The directories remain available through the services' runtime volume mounts; they are simply excluded from an image build that does not need them.

After adding the exclusions, both application images built successfully from their fixed base images and the corresponding PHP-FPM services were recreated.

6. Roadblock: a stale auxiliary PHP container

One environment also had a long-running, ad hoc command-line PHP container that was separate from the primary PHP-FPM service. Its image was older than the new runtime policy, and it was not replaced when the main web worker was recreated.

This was a useful reminder that a service inventory cannot stop at the primary web process. One-off jobs, administrative command-line containers, workers, scheduled tasks, and temporary debug shells can all retain an older runtime image.

The response was to stop the stale auxiliary container and update the declared command-line image reference alongside the web runtime. Subsequent ad hoc jobs will use the pinned, fixed image. For teams with recurring operational jobs, this should become an explicit deployment check rather than a manual cleanup step.

7. Verify the running container after every recreate

A successful image build does not prove that traffic is using the new image. A successful docker compose up does not prove that the application process started correctly. We validated the active runtime in every recreated service:

docker exec <php-container> php -r 'echo PHP_VERSION, PHP_EOL;'
Enter fullscreen mode Exit fullscreen mode

The final state was:

Service group Active runtime after rollout
Four PHP 8.3 services PHP 8.3.36
Two PHP 8.4 services PHP 8.4.27
One PHP-FPM content service PHP 8.4.27

We also rendered each Compose configuration before deployment to catch YAML, interpolation, and image-reference errors:

docker compose config -q
Enter fullscreen mode Exit fullscreen mode

The application image builds completed successfully after the build-context exclusions were added. Because the change was deliberately limited to the PHP base images and ignored build-only runtime artifacts, a full application test suite was not a substitute for, or prerequisite to, the key security assertion: the running PHP interpreter met the patched threshold.

8. What made the response effective

Several practices turned a fleet-wide response into a controlled maintenance task:

  1. Separate static evidence from runtime evidence. Dockerfiles identified where to investigate; php -v established what was actually exposed.
  2. Use fixed versions and immutable digests. Exact tags address patch level; digests protect against future tag drift.
  3. Change only the affected service. Recreating PHP workers without their dependencies reduced disruption.
  4. Treat build failures as deployment problems, not reasons to defer patching. The inaccessible runtime paths had a small, scoped solution.
  5. Include auxiliary processes in the inventory. A forgotten command-line PHP container can undermine an otherwise complete rollout.
  6. Verify after deployment. The only authoritative completion check was the version reported by each active container.

9. Follow-up controls

The immediate exposure was remediated, but the process should not depend on a manual emergency review every time a runtime advisory appears. The next controls are straightforward:

  • Maintain an inventory of public PHP workers, scheduled jobs, queue workers, and administrative containers.
  • Require exact PHP patch versions and registry digests in Dockerfiles and Compose files.
  • Add a deployment check that fails builds or releases below the approved PHP minimum.
  • Record the image digest and php -v output as a release artifact.
  • Review access and application logs during the relevant exposure window, then follow the incident-response process if compromise indicators appear.
  • Schedule regular base-image maintenance so urgent updates remain small, familiar changes.

Conclusion

The technical fix was not exotic: identify the active runtimes, select patched base images, rebuild application containers, recreate the affected services, and verify the live interpreter. The difficult part was maintaining the distinction between repository intent and runtime reality while responding quickly across multiple environments.

The result was a complete, verified rollout from vulnerable PHP patch levels to fixed releases, plus a more durable image-pinning and build-context practice. The next vulnerability response should begin with a clearer inventory, more deterministic image references, and fewer surprises between a successful build and a secure running service.

Top comments (0)