[php-src] Issue #22984: ext/soap: native stack overflow while decoding cyclic SOAP references
[email protected] (Amorsec) Sat, 1 Aug 2026 06:18:07 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22984
Author: Amorsec
### Description
### Summary
PHP's bundled SOAP extension does not detect cycles while resolving encoded SOAP
references. A SOAP response containing a cyclic reference can make the decoder
re-enter conversion of the same XML nodes until the native process stack is
exhausted. The behavior was confirmed for both SOAP 1.1 `href` references and
SOAP 1.2 `enc:ref` references.
The demonstrated impact is a denial of service: an application that decodes
such a response can terminate during SOAP response processing.
### Details
- **Affected component:** bundled `ext/soap` in PHP 8.4.20
- **Affected entry point:** `SoapClient::__soapCall()` while decoding a SOAP response
- **Affected source:** `https://github.com/php/php-src`, PHP 8.4.20;
`ext/soap/php_encoding.c` and `ext/soap/php_xml.c`
- **Root cause:** encoded-reference resolution returns a referenced XML node
to the normal value converter without tracking the nodes already active in
the current resolution chain.
- **Trigger condition:** a SOAP response contains a cyclic encoded-reference
relationship and the referenced values are recursively converted.
For SOAP 1.1, `check_and_resolve_href()` resolves an internal
`href="#id"` by searching the document for a node with the matching
unqualified `id` attribute. The returned node is passed to
`master_to_zval()`. If that node contains a child whose `href` points back
to an active node, conversion enters `check_and_resolve_href()` again.
The relevant SOAP 1.1 path is:
```c
if (href->children->content[0] == '#') {
xmlNodePtr ret = get_node_with_attribute_recursive(
data->doc->children, NULL, "id",
(char *)&href->children->content[1]);
if (!ret) {
soap_error1(E_ERROR, "Encoding: Unresolved reference '%s'",
href->children->content);
}
return ret;
}
```
SOAP 1.2 uses namespaced `enc:ref` and `enc:id` attributes. The decoder
rejects a direct self-reference when the resolved node is exactly the current
node, but it does not maintain an active-reference set. A cycle involving two
or more nodes therefore remains possible:
```c
ret = get_node_with_attribute_recursive_ex(
data->doc->children, NULL, NULL, "id", (char *)id,
SOAP_1_2_ENC_NAMESPACE);
if (!ret) {
soap_error1(E_ERROR, "Encoding: Unresolved reference '%s'",
href->children->content);
} else if (ret == data) {
soap_error1(E_ERROR, "Encoding: Violation of id and ref information items '%s'",
href->children->content);
}
return ret;
```
Both paths are called at the start of `master_to_zval()`:
```c
zval *master_to_zval(zval *ret, encodePtr encode, xmlNodePtr data)
{
data = check_and_resolve_href(data);
...
return master_to_zval_int(ret, encode, data);
}
```
During array conversion, each child is sent back through `master_to_zval()`.
A cyclic reference consequently causes the same conversion sequence to repeat:
```c
master_to_zval(&tmpVal, enc, trav);
```
The recursive XML lookup is implemented by
`get_node_with_attribute_recursive_ex()` in `ext/soap/php_xml.c:298-316`.
The unbounded behavior in this finding is the repeated reference conversion;
the finite XML lookup is re-entered as part of each conversion.
### PoC
#### Environment and configuration
- **Container/image:** `php-asan-8420-dbase-build`, based on
`php-asan:8.4.20`
- **PHP:** PHP 8.4.20 CLI, NTS, DEBUG, AddressSanitizer-instrumented
- **PHP API:** `20240924`
- **PHP source revision:** PHP 8.4.20 source tree; no commit identifier was
retained
- **PHP configure options:**
```sh
./configure \
--enable-embed=static \
--enable-debug \
--disable-cgi \
--disable-phpdbg \
--without-pear \
--disable-all \
--with-libxml \
--enable-session \
--with-pic \
--enable-spl=shared \
--enable-json=shared \
--enable-tokenizer=shared \
--enable-dom=shared \
--enable-simplexml=shared \
--enable-xml=shared \
--enable-xmlreader=shared \
--enable-soap=shared \
--with-xsl=shared \
--with-tidy=shared
```
- **PHP compiler/linker flags:** not retained in the container record; the
loaded SOAP module and PHP binary were built with AddressSanitizer
- **Extension:** bundled `ext/soap`, enabled as a shared module; no separate
PECL SOAP module is loaded
- **SOAP module:** `/build-structured-fuzz/modules/soap.so`
- **SOAP module SHA-256:**
`6a6db7cc99e174ba65b74d81332a2582395a81064c0161f330abd104cc6b0e8b`
- **Sanitizer runtime:** the captured runs used the embedded ASan driver and
completed with `timed_out=false` and `cleanup_ok=true`
#### Reproducer artifacts
The directory contains two protocol-specific PHP reproducer files:
- `SOAP-NEW-010_soap11_poc.php` returns a SOAP 1.1 response containing an
internal `href` cycle to the normal `SoapClient` response decoder.
- `SOAP-NEW-010_soap12_poc.php` returns a SOAP 1.2 response containing an
indirect two-node `enc:ref` cycle to the same decoder.
The PoCs use `SoapClient::__doRequest()` only to provide a deterministic
response in the test environment. The observed fault is in the bundled SOAP
decoder. The exact source files are retained in this directory together with
the environment record and sanitizer excerpts.
#### Sanitizer report
The SOAP 1.1 run returned status `42` and produced:
```text
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2653432==ERROR: AddressSanitizer: stack-overflow
#0 __interceptor_strcmp
#1 attr_is_equal_ex /build-structured-fuzz/src/ext/soap/php_xml.c:208:39
#2 get_attribute_ex /build-structured-fuzz/src/ext/soap/php_xml.c:242:7
#3 to_zval_array /build-structured-fuzz/src/ext/soap/php_encoding.c:2527:2
#4 master_to_zval_int /build-structured-fuzz/src/ext/soap/php_encoding.c:563:9
#5 guess_zval_convert /build-structured-fuzz/src/ext/soap/php_encoding.c:2901:2
#6 master_to_zval_int /build-structured-fuzz/src/ext/soap/php_encoding.c:563:9
#7 master_to_zval /build-structured-fuzz/src/ext/soap/php_encoding.c:599:9
#8 to_zval_array /build-structured-fuzz/src/ext/soap/php_encoding.c:2672:4
... repeated conversion frames ...
SUMMARY: AddressSanitizer: stack-overflow in __interceptor_strcmp
```
The SOAP 1.2 two-node cycle also returned status `42` and produced:
```text
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2653500==ERROR: AddressSanitizer: stack-overflow
#0 __interceptor_strcmp
#1 attr_is_equal_ex /build-structured-fuzz/src/ext/soap/php_xml.c:208:39
#2 get_attribute_ex /build-structured-fuzz/src/ext/soap/php_xml.c:242:7
#3 to_zval_array /build-structured-fuzz/src/ext/soap/php_encoding.c:2527:2
#4 master_to_zval_int /build-structured-fuzz/src/ext/soap/php_encoding.c:563:9
#5 master_to_zval /build-structured-fuzz/src/ext/soap/php_encoding.c:599:9
#6 to_zval_array /build-structured-fuzz/src/ext/soap/php_encoding.c:2672:4
#7 master_to_zval_int /build-structured-fuzz/src/ext/soap/php_encoding.c:563:9
#8 master_to_zval /build-structured-fuzz/src/ext/soap/php_encoding.c:599:9
... repeated conversion frames ...
SUMMARY: AddressSanitizer: stack-overflow in __interceptor_strcmp
```
The complete retained excerpts are
`SOAP-NEW-010_soap11_asan.txt` and
`SOAP-NEW-010_soap12_asan.txt`. Both records show
`returncode=42`, `timed_out=false`, and `cleanup_ok=true`.
[SOAP-NEW-010_soap11_asan.txt](https://github.com/user-attachments/files/30612453/SOAP-NEW-010_soap11_asan.txt)
[SOAP-NEW-010_soap11_poc.php](https://github.com/user-attachments/files/30612455/SOAP-NEW-010_soap11_poc.php)
[SOAP-NEW-010_soap12_asan.txt](https://github.com/user-attachments/files/30612452/SOAP-NEW-010_soap12_asan.txt)
[SOAP-NEW-010_soap12_poc.php](https://github.com/user-attachments/files/30612454/SOAP-NEW-010_soap12_poc.php)
### PHP Version
```plain
PHP 8.4.20
```
### Operating System
_No response_