01. Observed Symptoms & Why Standard Package Audits Fail

Observed (identifiers): This post uses the identifiers recorded in HadayaLab's intake: CVE-2026-109618, CVE-2026-109619, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp, the function zend_handle_delayed_oom(), and commit 3584a98bda. The authorities are the PHP announcement (https://www.php.net/archive/2026.php#2026-10-21-5), the PHP 8 ChangeLog (https://www.php.net/ChangeLog-8.php), and the two GitHub advisories. Re-read each against the live page before you act on it.

Symptoms that should trigger a runtime check, not a dependency check: (1) composer audit, Dependabot and your WordPress scanner are all clean, yet an advisory says PHP itself is affected. (2) Nginx returns intermittent 502/504 while php-fpm workers exit or get OOM-killed. (3) New or modified .php files appear in uploads/, storage/ or wp-content/. (4) Your hosting console says 'PHP 8.3, up to date' but nobody has confirmed that each live instance restarted onto the new build.

Observed (tooling behaviour): composer audit evaluates the packages in composer.lock against an advisory database. The PHP interpreter is a platform requirement, not a scanned package. WordPress plugin scanners check plugin and theme versions. Neither tool reads the memory image of the running php-fpm master.

Observed (platform identifier): The App Engine runtime string php83_20261020_8_3_33_RC02 is recorded in our intake as the 'un-restarted worker trap'. Inference: App Engine instances keep the runtime image they started on until they are recycled or the version is redeployed, so a service-level 'latest runtime' label does not prove that every live instance is patched. Check what each instance runs, not what the console shows for the service. Whether that build contains the fix is a question for the ChangeLog and the commit lookup in Section 02, not for the label.

02. Root-Cause Forensic Analysis (Runtime & Protocol Level)

Observed: The advisory metadata names zend_handle_delayed_oom() and commit 3584a98bda. Inference (from naming only): the zend_ prefix places the code in the Zend Engine core, and 'delayed OOM' points at memory-exhaustion handling. If that reading holds, the flaw is a memory-safety class defect in the engine. It is reachable regardless of framework (Laravel, Symfony, WordPress, WooCommerce) and invisible to any tool that inspects PHP code or Composer packages. Treat this as a hypothesis until you have read the commit diff.

Do not take fixed version numbers from a blog post, including this one. Resolve them mechanically: the commit hash maps to the release tags that contain it. The script below does that, then checks what is actually executing.

Inference (operational): php-fpm workers are long-lived processes forked from a master. Upgrading the package on disk does not change the binary mapped by an already-running master. After an upgrade, /proc//exe resolving to a path marked '(deleted)' means the old binary is still serving requests. Restart the service, not just the file.

Verify the running binary, then derive fixed releases from commit 3584a98bda BASH
#!/usr/bin/env bash
set -euo pipefail
# 1. What is actually serving traffic (run inside the container / instance)
php -r 'echo PHP_VERSION, PHP_EOL;'
php-fpm -v | head -n1
readlink /proc/$(pgrep -o -f 'php-fpm: master')/exe   # '(deleted)' suffix = package upgraded, master never restarted
# 2. Which release tags contain the fix commit (authoritative; no guessing)
git clone --filter=blob:none --no-checkout https://github.com/php/php-src.git /tmp/php-src
git -C /tmp/php-src tag --contains 3584a98bda | grep -E '^php-8[.][34][.]' | sort -V | head -n 6
# 3. Read the change itself before trusting any summary
git -C /tmp/php-src show 3584a98bda --stat
1:1 EMBEDDED SOLUTION PIPELINE · HL-R1-PHP2SHELL

5-Register Quarantine & Attestation Engine

AWAITING INSPECTION
REGISTER 1 In-Browser AST & Lockfile Inspector
$0 · ALWAYS FREE

Evaluated 100% locally in your browser inside a zero-exfiltration sandbox (connect-src 'none'). Your credentials and proprietary code never touch our servers.

REGISTER 2 Non-Breaking Vaccine & Config Diff
10 cr ($1.00)

Generates targeted unified .patch diffs and perimeter WAF rules. Non-breaking compatibility is mathematically verified against the complete dependency graph.

✓ NON-BREAKING VERIFIED: Compatible across 14 downstream dependencies (0 semver conflicts)
TARGET FILES: CRYPTOGRAPHICALLY VERIFIED
/etc/php/8.3/fpm/pool.d/www.conf & php.ini
Zero-Downtime Session Upload Progress & Buffer Overflow Mitigation (CVE-2026-109618)
REGISTER 3 Signed CycloneDX 1.6 SBOM & Attestation PDF
20 cr ($2.00)

Official cryptographic audit certificate with DSSE Ed25519 digital signature, 7-day rolling attestation window, and compliance mapping for NIST CSF 2.0 and SOC2.

ATTESTATION SPEC: No Findings in Scope
Auditor Authority: 45x Google Cloud & DeepMind Skill Badges
Attestation Window: Valid for 7 days (next variant sweep required in 7 days)
Signature Spec: in-toto Statement v1 / DSSE Ed25519 envelope (hadayalab-attest-2026-ed25519-k1)
Inspect Sample Ed25519 Signed SBOM (/sbom/PAIN-2026-001.cdx.json) →
REGISTER 4 Side-Effect & Rollback Manifest
10 cr ($1.00)

Instant crash and compatibility analyzer. Computes peerDependency, PHP extension, and engine compatibility changes with a 1-command rollback manifest.

ROLLBACK INTEGRITY SPEC:
Atomic git-level rollback manifest with state-reversal guarantees for PAIN-2026-001.
REGISTER 5 Continuous CI/CD GitHub Actions OIDC Sweeper
1 cr / run ($0.10)

Automated weekly variant sweep via GitHub Actions OIDC (id-token: write). Auto-renews 7-day rolling attestation tokens without long-lived secrets.

AUTOMATION TARGET:
.github/workflows/hadayalab-attest.yml
Zero long-lived API secrets · OIDC Short-Lived JWT Verification

03. Immediate Zero-Downtime Triage (Non-Breaking Mitigation)

Inference: The controls below reduce attack surface while you schedule the runtime upgrade. They come from HadayaLab's triage profile for this advisory pair. Confirm applicability against the advisory text. They do not replace the patch.

Step 1, PHP-FPM: disable the session upload-progress path. Most applications never use it. A progress bar that polls $_SESSION for upload state is the only common casualty. Validate the config, then reload. A reload re-reads pool configuration without dropping in-flight requests, but it is not proof that the binary was swapped.

Step 2, Nginx: execute PHP only through the front controller, and never from writable paths. This limits what a post-exploitation write can run.

Step 3, Google App Engine: force fresh instances onto the current runtime by redeploying a new version and stopping the old one. Do not assume the console's runtime label applies to already-running instances.

PHP-FPM pool (e.g. /etc/php/8.3/fpm/pool.d/www.conf) INI
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[expose_php] = off

; validate, then apply without dropping in-flight requests:
;   php-fpm8.3 -tt && systemctl reload php8.3-fpm
Nginx: restrict PHP execution NGINX
# Laravel / Symfony: only the front controller may execute PHP
location = /index.php {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param PHP_ADMIN_VALUE 'session.upload_progress.enabled=0';
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ \.php$ { return 404; }

# WordPress: do NOT use the catch-all above (it breaks wp-login.php). Use this instead:
# location ~* ^/wp-content/uploads/.*\.php$ { deny all; }
Google App Engine: replace stale instances BASH
gcloud app versions list --service=default
gcloud app instances list --service=default
# redeploy forces fresh instances on the current runtime image
gcloud app deploy app.yaml --version=v20261024 --promote --stop-previous-version
# for any instance you cannot drain via redeploy:
gcloud app instances delete INSTANCE_ID --service=default --version=OLD_VERSION

04. Permanent Remediation & Cryptographic SBOM Attestation

Permanent fix: move every environment to a PHP 8.3 or 8.4 release that contains commit 3584a98bda (use the tag lookup in Section 02 and the ChangeLog), then restart. Do not rely on a package version string alone. Assert the version at runtime in your post-deploy gate, as shown below, so a stale worker fails the pipeline instead of passing it silently.

Attestation: record the runtime in a CycloneDX SBOM, sign it, and store the signature next to the release artifact. Article PAIN-2026-005 covers the container pipeline end to end (SHA-256 digest pinning, SBOM, Binary Authorization). The audit trail you want is: commit hash in, fixed tag resolved, runtime version asserted, SBOM signed.

Primary sources: https://www.php.net/archive/2026.php#2026-10-21-5 | https://www.php.net/ChangeLog-8.php | https://github.com/advisories/GHSA-w8hw-w9fx-j79w | https://github.com/advisories/GHSA-6wmf-4rv5-p3rp

HadayaLab operates with 45x Google Cloud & DeepMind Skill Badges (https://www.skills.google/public_profiles/49cdbdb6-20d4-4661-934a-d882c7f5c6cf). We run fixed-scope PHP runtime audits covering Laravel, WordPress/WooCommerce, Nginx/PHP-FPM and App Engine, with evidence output you can hand to an auditor.

Post-deploy gate: fail the pipeline on a stale runtime BASH
export FIXED=8.3.X   # replace with the tag resolved from commit 3584a98bda, never from memory
php -r 'exit(version_compare(PHP_VERSION, getenv("FIXED"), ">=") ? 0 : 1);' || { echo 'PHP runtime below fixed release'; exit 1; }

Frequently Asked Questions

Does composer audit detect CVE-2026-109618 or CVE-2026-109619?

No. composer audit checks the packages in composer.lock against an advisory database. These CVEs are filed against the PHP engine. Verify the interpreter by running php -v inside the live container or instance and comparing it with the fixed tags you resolve from commit 3584a98bda.

How do I find which PHP 8.3 / 8.4 release fixes PHP2Shell without trusting a third-party summary?

Clone php-src and run git tag --contains 3584a98bda. Every tag in the output contains the fix. Cross-check it against https://www.php.net/ChangeLog-8.php and the two GitHub advisories (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp).

My Laravel app passed its last security audit. Why would I still be exposed?

An application-level audit tests your code and dependencies. An engine-level defect lives below both. Passing that audit says nothing about the PHP binary your workers run, or about whether those workers were restarted after the runtime was updated.

Will disabling session.upload_progress break my WordPress or WooCommerce site?

In most deployments, no. The setting only affects code that reads upload progress from the session, such as custom progress bars. Test it on a canary pool first. It is a compensating control, not a patch, so schedule the runtime upgrade regardless.