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:
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:
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():
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:
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:
./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:
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:
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
SOAP-NEW-010_soap11_poc.php
SOAP-NEW-010_soap12_asan.txt
SOAP-NEW-010_soap12_poc.php
PHP Version
Operating System
No response
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
hrefreferences andSOAP 1.2
enc:refreferences.The demonstrated impact is a denial of service: an application that decodes
such a response can terminate during SOAP response processing.
Details
ext/soapin PHP 8.4.20SoapClient::__soapCall()while decoding a SOAP responsehttps://github.com/php/php-src, PHP 8.4.20;ext/soap/php_encoding.candext/soap/php_xml.cto the normal value converter without tracking the nodes already active in
the current resolution chain.
relationship and the referenced values are recursively converted.
For SOAP 1.1,
check_and_resolve_href()resolves an internalhref="#id"by searching the document for a node with the matchingunqualified
idattribute. The returned node is passed tomaster_to_zval(). If that node contains a child whosehrefpoints backto an active node, conversion enters
check_and_resolve_href()again.The relevant SOAP 1.1 path is:
SOAP 1.2 uses namespaced
enc:refandenc:idattributes. The decoderrejects 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:
Both paths are called at the start of
master_to_zval():During array conversion, each child is sent back through
master_to_zval().A cyclic reference consequently causes the same conversion sequence to repeat:
The recursive XML lookup is implemented by
get_node_with_attribute_recursive_ex()inext/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
php-asan-8420-dbase-build, based onphp-asan:8.4.2020240924retained
loaded SOAP module and PHP binary were built with AddressSanitizer
ext/soap, enabled as a shared module; no separatePECL SOAP module is loaded
/build-structured-fuzz/modules/soap.so6a6db7cc99e174ba65b74d81332a2582395a81064c0161f330abd104cc6b0e8bcompleted with
timed_out=falseandcleanup_ok=trueReproducer artifacts
The directory contains two protocol-specific PHP reproducer files:
SOAP-NEW-010_soap11_poc.phpreturns a SOAP 1.1 response containing aninternal
hrefcycle to the normalSoapClientresponse decoder.SOAP-NEW-010_soap12_poc.phpreturns a SOAP 1.2 response containing anindirect two-node
enc:refcycle to the same decoder.The PoCs use
SoapClient::__doRequest()only to provide a deterministicresponse 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
42and produced:The SOAP 1.2 two-node cycle also returned status
42and produced:The complete retained excerpts are
SOAP-NEW-010_soap11_asan.txtandSOAP-NEW-010_soap12_asan.txt. Both records showreturncode=42,timed_out=false, andcleanup_ok=true.SOAP-NEW-010_soap11_asan.txt
SOAP-NEW-010_soap11_poc.php
SOAP-NEW-010_soap12_asan.txt
SOAP-NEW-010_soap12_poc.php
PHP Version
Operating System
No response