Skip to content

ext/soap: native stack overflow while decoding cyclic SOAP references #22984

Description

@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:

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

PHP 8.4.20

Operating System

No response

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions