Like a spring-loaded jack-in-the-box that keeps unfolding until it breaks the shelf — except the shelf is your PHP worker's call stack
CVE-2026-91765 is an unbounded-recursion flaw in PHP's built-in SOAP extension. Three functions — cleanup_xml_node() (XML tree cleanup), master_to_zval_int() (value decoder following href chains), and get_node_with_attribute_recursive_ex() (WSDL node search) — all recurse once per XML nesting level with no depth guard. An unauthenticated attacker can POST a SOAP envelope containing tens of thousands of nested elements to any reachable SoapServer endpoint. The C-level recursion eats stack frames until the process hits the OS stack limit and segfaults. Under PHP-FPM the crashed worker is replaced, but a sustained stream of such requests drains the pool and takes the endpoint offline. Affected versions are PHP 8.2 < 8.2.34, 8.3 < 8.3.35, 8.4 < 8.4.26, and 8.5 < 8.5.11. The fix (shipped in those releases) introduces a SOAP_MAX_XML_DEPTH constant capped at 2048, converts the recursive helpers to iterative loops, and adds depth tracking in SOAP globals to break href cycles.
The vendor rates this HIGH 7.5, and the CVSS math is technically correct for the vector (network / no auth / no complexity / availability-only). However, the vendor score overstates the real-world urgency for most enterprises. The impact ceiling is a process crash — there is zero path to code execution, data exfiltration, or lateral movement. The attack surface is limited to the subset of PHP installations that (a) load the SOAP extension *and* (b) expose a SoapServer endpoint reachable by untrusted clients. Many PHP deployments use SOAP only as a *client* (SoapClient), which is not affected. PHP-FPM worker pools recover automatically, so achieving a sustained outage requires sustained attack traffic — something a basic rate limiter or WAF XML-depth rule defeats. EPSS is 0.55% (≈43rd percentile), there is no CISA KEV listing, no confirmed in-the-wild exploitation, and no published proof-of-concept code. noisgate downgrades this to MEDIUM 6.5.
4 steps from start to impact.
Identify a reachable SoapServer endpoint
/soap, /api/soap, /ws/, or documented WSDL URLs. Shodan shows ~1.9 million internet-facing PHP hosts, but only a fraction run SoapServer. On shared hosting with cPanel/DirectAdmin/Plesk, the SOAP extension is loaded by default, but the *server* class still has to be instantiated by the application.- Target PHP application loads ext/soap
- Target instantiates SoapServer (not just SoapClient)
- SoapServer endpoint reachable from the internet or attacker's network position
- Most PHP apps use frameworks (Laravel, Symfony) that don't use SoapServer — REST/JSON dominates
- Internal SOAP APIs are often behind VPN or not exposed to the internet
- Application firewalls may block or limit SOAP traffic by default
X-Powered-By: PHP combined with WSDL path probing can identify candidates.Craft a deeply nested XML SOAP payload
<a><a><a>.... The payload is compact: 50,000 levels of <a> is under 300 KB uncompressed. With gzip transfer encoding (standard for HTTP), the wire size drops to a few KB. No special tooling is needed; a Python one-liner or bash printf loop suffices. No authentication or valid SOAP action is required — the parser processes the XML structure before any application-level validation.- Ability to send HTTP POST to the target endpoint
- No request-size or XML-depth filtering by upstream WAF
- Enterprise WAFs (F5 ASM, Cloudflare, AWS WAF) with XML-depth inspection rules will block this
- ModSecurity with the CRS ruleset caps XML body depth by default
- Content-length limits on reverse proxies may reject very large nesting if uncompressed
Send the payload and crash the PHP worker
cleanup_xml_node() begins its recursive walk. With ~50K nesting levels, the function consumes ~400 bytes of stack per frame (platform-dependent), exhausting the default 8 MB stack in under 20,000 frames. The process receives SIGSEGV and dies. The request takes milliseconds to deliver but the crash is instantaneous once parsing begins.- Payload reaches PHP's SOAP parser without depth truncation
- PHP-FPM's
pm.max_childrenlimits concurrency — killing one worker doesn't kill the pool - systemd/cgroup OOM-killer or stack-size ulimits may vary the crash threshold
- Process managers restart workers automatically
children ... exited on signal 11 (SIGSEGV). Monitoring for abnormal worker churn (e.g., Prometheus phpfpm_slow_requests or process-restart counters) catches this quickly.Sustain the attack to exhaust the worker pool
pm.max_children = 50 and each crash taking ~100ms round-trip, the attacker needs roughly 500 requests/second to keep the pool drained. This is a modest volumetric requirement. However, any rate-limiting, connection throttling, or IP-based blocking at the load balancer or WAF layer will break the attack loop. The DoS persists only while attack traffic flows — there is no persistence mechanism.- Sustained network access to the endpoint
- No rate limiting or IP blocking at the network edge
- CDN/WAF rate limiting is standard for internet-facing applications
- Geo-blocking or IP reputation lists reduce attacker pool
- Auto-scaling infrastructure (k8s HPA, cloud ASG) can absorb worker churn
- Incident response team can null-route attacker IPs within minutes
phpfpm_process_restarts > 10/min AND http_path contains 'soap'.SecRule XML:/* "@gt 256") provides this out of the box. This breaks the attack at step 2 — the payload never reaches PHP. No noisgate mitigation SLA applies for MEDIUM, so go straight to the 365-day remediation window, but this control is quick to deploy and highly effective.limit_req, HAProxy stick-tables, Cloudflare rate rules). Breaks the attack at step 4.SoapClient (outbound SOAP calls) or doesn't use SOAP at all, remove the extension entirely: comment out extension=soap in php.ini or apt remove php-soap. This eliminates the vulnerable code path completely. Audit with php -m | grep -i soap across the fleet. On shared hosting, request the provider disable it if unused.SOAP_MAX_XML_DEPTH=2048 and converts recursive functions to iterative loops. Per the noisgate remediation SLA for MEDIUM, complete within 365 days. Coordinate with your next PHP minor-version maintenance window.- PHP
memory_limit(ini_set) — does not help. The crash is caused by *stack* exhaustion (SIGSEGV), not *heap* exhaustion (OOM). PHP's memory_limit governs heap allocations only; the C-level call stack is bounded by the OSulimit -s, which PHP cannot control at runtime. max_input_time/max_execution_time— does not help. The crash occurs during XML parsing *before* the PHP script's execution timer starts. The process segfaults in libxml/C land, not in userspace PHP.libxml_disable_entity_loader()— irrelevant. This CVE is about recursion depth in tree traversal, not XXE or entity expansion. Disabling external entities has no effect on nesting depth.
The supporting signals.
| In-the-Wild Exploitation | None confirmed. Not listed in CISA KEV. No campaigns or threat-actor usage documented as of 2026-10-25. |
|---|---|
| Proof-of-Concept | No public PoC published. Mallory.ai and TheHackerWire both report filtering out candidate repos as fakes or README-only. However, the exploit is *trivially constructable* — a loop printing nested <a> tags is sufficient. Expect weaponized PoCs within weeks. |
| EPSS | 0.55% probability of exploitation in the next 30 days, approximately 43rd–45th percentile. Low exploitation likelihood relative to the broader CVE population. |
| CISA KEV | Not listed. No federal mandate for emergency patching under BOD 22-01. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — Network-accessible, no auth, no complexity, availability-only impact. The vector correctly reflects the technical attack surface but overstates operational risk because it cannot model the SoapServer-subset constraint or PHP-FPM recovery. |
| Affected Versions | PHP 8.2.0–8.2.33, 8.3.0–8.3.34, 8.4.0–8.4.25, 8.5.0–8.5.10. Older EOL branches (8.1 and below) are likely affected but will not receive patches. |
| Fixed Versions | PHP 8.2.34, 8.3.35, 8.4.26, 8.5.11. Distro backports: Debian bookworm 8.2.35-1~deb12u1 (DLA-4819-1), Debian trixie 8.4.27-1~deb13u1 (DSA-6514-1), Fedora 43 8.4.27, Amazon Linux 2 ALAS2PHP8.2-2026-014. RHEL status under investigation. |
| Scanning / Exposure | Shodan reports ~1.9 million internet-facing PHP hosts. SOAP extension is loaded by default on cPanel/DirectAdmin/Plesk shared hosting. However, only a fraction of these instantiate SoapServer — most modern PHP apps use REST/JSON. No GreyNoise tags or mass-scanning activity observed for this CVE. |
| Disclosure & Credit | Disclosed 2026-09-25. Reported by ExPatch-LLC. Fix developed by alexandre-daubois (php-src contributor). GitHub advisory GHSA-rgrp-mwpx-f6rm. |
| Related SOAP CVEs (2026) | CVE-2026-6722 (SOAP RCE via use-after-free, CVSS 9.5), CVE-2026-7261 (SOAP use-after-free in session persistence), CVE-2026-7262 (SOAP NULL-ptr DoS). If you run SoapServer, patch the entire batch together. |
Sources.
Why this verdict
- DoS-only impact ceiling: The vulnerability's maximum outcome is a process crash and transient service disruption. There is zero path to code execution, memory disclosure, data exfiltration, or lateral movement. The CVSS Confidentiality and Integrity sub-scores are both None. This fundamentally caps the operational severity below what a HIGH rating implies to a patch-prioritization audience.
- Attack surface narrowing: The flaw requires a running
SoapServerinstance reachable by the attacker — not merely a PHP server with ext/soap loaded. Most modern PHP applications use REST/JSON frameworks (Laravel, Symfony, WordPress REST API) and never instantiateSoapServer. The reachable population is a small subset of the ~1.9M internet-facing PHP hosts, concentrated in legacy enterprise integrations and payment gateways. - PHP-FPM automatic recovery: Crashed workers are respawned by the process manager within milliseconds. Achieving sustained denial of service requires *continuous* attack traffic at ~500 req/s for a typical 50-worker pool. This is not a 'fire and forget' crash — it is a volumetric DoS that any rate limiter, WAF, or CDN throttle defeats.
- No exploitation evidence: EPSS is 0.55% (43rd percentile), no CISA KEV listing, no confirmed in-the-wild activity, no published PoC. The trivial nature of the exploit means this could change, but current threat intelligence does not support emergency treatment.
- Role multiplier: PHP runs across workstations (dev), typical web app servers, and high-value roles (e-commerce payment gateways, ERP SOAP connectors, healthcare HL7/SOAP bridges). On high-value roles, the chain succeeds if SoapServer is exposed and unprotected. However, the blast radius even on a payment gateway is *host-level availability disruption* — not domain takeover, fleet compromise, or data exfiltration. A sustained outage of a payment SOAP endpoint is operationally painful but does not cross the HIGH floor threshold (which requires domain/fleet/supply-chain/safety-scale outcomes). Enterprise SOAP endpoints in high-value roles are typically behind WAFs and rate limiters, further reducing realistic impact.
Why not higher?
Upgrading to HIGH would require either a path beyond availability impact (RCE, data leak, lateral movement) or evidence of active exploitation making the theoretical risk concrete. Neither condition is met. The DoS impact on even high-value PHP SOAP endpoints caps at service disruption — painful but recoverable, and trivially mitigable with a WAF XML-depth rule or rate limiter. No threat actor has weaponized this, and EPSS confirms low exploitation probability.
Why not lower?
Dropping to LOW would understate the risk for the minority of enterprises that *do* expose SoapServer endpoints to untrusted networks. The attack is unauthenticated, requires no user interaction, has no complexity barrier, and the exploit is trivially constructable even without a published PoC. A single HTTP POST can crash a worker instantly. For organizations running legacy SOAP integrations on internet-facing PHP, this is a genuine operational risk that warrants patching within a year, not indefinite deferral.
Crowdsourced verification payload.
Run this script on each target PHP host (web server or container) as any user with permission to execute php. No root required. Invoke: bash check_cve_2026_91765.sh — it checks the PHP version, whether ext/soap is loaded, and whether the fix constant SOAP_MAX_XML_DEPTH exists.
#!/usr/bin/env bash
# check_cve_2026_91765.sh — Detect CVE-2026-91765 (PHP SOAP unbounded recursion)
# Run on the target PHP host. No root needed.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
PHP_BIN="${1:-php}"
# Check PHP binary exists
if ! command -v "$PHP_BIN" &>/dev/null; then
echo "UNKNOWN — '$PHP_BIN' not found in PATH"
exit 2
fi
# Check if SOAP extension is loaded
SOAP_LOADED=$( "$PHP_BIN" -m 2>/dev/null | grep -ci '^soap$' || true )
if [ "$SOAP_LOADED" -eq 0 ]; then
echo "PATCHED — ext/soap is not loaded; CVE-2026-91765 is not reachable on this host"
exit 0
fi
# Get PHP version
PHP_VERSION=$( "$PHP_BIN" -r 'echo PHP_VERSION;' 2>/dev/null )
if [ -z "$PHP_VERSION" ]; then
echo "UNKNOWN — could not determine PHP version"
exit 2
fi
echo "Detected PHP $PHP_VERSION with ext/soap loaded"
# Check for the fix constant SOAP_MAX_XML_DEPTH (introduced in the patch)
HAS_FIX=$( "$PHP_BIN" -r 'echo defined("SOAP_MAX_XML_DEPTH") ? "yes" : "no";' 2>/dev/null )
if [ "$HAS_FIX" = "yes" ]; then
echo "PATCHED — SOAP_MAX_XML_DEPTH constant present (fix applied)"
exit 0
fi
# Fallback: version comparison for known-fixed versions
# Fixed: 8.2.34, 8.3.35, 8.4.26, 8.5.11
version_gte() {
# Returns 0 if $1 >= $2
printf '%s\n%s' "$2" "$1" | sort -V -C
}
MAJOR_MINOR=$( echo "$PHP_VERSION" | grep -oP '^\d+\.\d+' )
case "$MAJOR_MINOR" in
8.2) FIXED="8.2.34" ;;
8.3) FIXED="8.3.35" ;;
8.4) FIXED="8.4.26" ;;
8.5) FIXED="8.5.11" ;;
*)
# PHP 8.1 and below are EOL and likely vulnerable but won't get patches
if version_gte "$PHP_VERSION" "9.0.0"; then
echo "UNKNOWN — PHP $PHP_VERSION is newer than known affected range"
exit 2
fi
echo "VULNERABLE — PHP $PHP_VERSION (EOL branch) with ext/soap loaded; no patch available"
exit 1
;;
esac
if version_gte "$PHP_VERSION" "$FIXED"; then
echo "PATCHED — PHP $PHP_VERSION >= $FIXED"
exit 0
else
echo "VULNERABLE — PHP $PHP_VERSION < $FIXED with ext/soap loaded"
exit 1
fi