Skip to content

Commit 41f2ca4

Browse files
LamentXU123devnexenNickSdot
authored
ext/soap: Fix SOAP classmap validation for integer keys (#22884)
Previously, SOAP only rejected packed arrays, which still allowed sparse integer-keyed arrays and mixed string integer-keyed arrays to be accepted. Since classmap is expected to be an associative mapping, arrays containing integer keys are now rejected consistently. Co-authored-by: David CARLIER <devnexen@gmail.com> Co-authored-by: NickSdot <32384907+NickSdot@users.noreply.github.com>
1 parent 22a6f16 commit 41f2ca4

10 files changed

Lines changed: 254 additions & 26 deletions

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ PHP NEWS
7979
- SOAP:
8080
. Fixed header injection through the Content-Type context option, the
8181
soapaction and the cookie names and values. (David Carlier)
82+
. Fixed the SoapClient and SoapServer "classmap" option to reject arrays
83+
containing integer keys, and made SoapClient throw TypeError/ValueError
84+
for invalid "classmap" options. (Weilin Du, David Carlier)
8285

8386
- MBString:
8487
. Fixed bug GH-22779 (mb_strrpos() returns the wrong position for a negative

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ PHP 8.6 UPGRADE NOTES
160160
system.
161161

162162
- SOAP:
163+
. The "classmap" option of SoapClient and SoapServer now rejects arrays
164+
containing integer keys. Previously, sparse integer-keyed and mixed-keyed
165+
arrays could be accepted. SoapClient now throws TypeError for non-array
166+
"classmap" options and ValueError for arrays containing integer keys, also
167+
when the "exceptions" option is disabled.
163168
. WSDL/XML Schema parsing now rejects out-of-range integer values for
164169
occurrence constraints and integer restriction facets. Negative minOccurs
165170
and maxOccurs values are rejected as well.

ext/soap/php_encoding.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ static xmlNodePtr master_to_xml_int(encodePtr encode, zval *data, int style, xml
440440
zval *tmp;
441441
zend_string *type_name;
442442

443-
ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(SOAP_GLOBAL(class_map), type_name, tmp) {
443+
ZEND_HASH_FOREACH_STR_KEY_VAL(SOAP_GLOBAL(class_map), type_name, tmp) {
444444
ZVAL_DEREF(tmp);
445445
if (Z_TYPE_P(tmp) == IS_STRING &&
446446
ZSTR_LEN(ce->name) == Z_STRLEN_P(tmp) &&

ext/soap/soap.c

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,18 @@ static HashTable* soap_create_typemap(sdlPtr sdl, HashTable *ht) /* {{{ */
930930
}
931931
/* }}} */
932932

933+
static bool soap_class_map_has_only_string_keys(const HashTable *class_map)
934+
{
935+
zend_string *key;
936+
ZEND_HASH_FOREACH_STR_KEY(class_map, key) {
937+
if (UNEXPECTED(key == NULL)) {
938+
return false;
939+
}
940+
} ZEND_HASH_FOREACH_END();
941+
942+
return true;
943+
}
944+
933945
/* {{{ SoapServer constructor */
934946
PHP_METHOD(SoapServer, __construct)
935947
{
@@ -1007,8 +1019,7 @@ PHP_METHOD(SoapServer, __construct)
10071019
zend_argument_type_error(2, "\"classmap\" option must be of type array, %s given", zend_zval_type_name(class_map_zv));
10081020
goto cleanup;
10091021
}
1010-
// TODO: this still accepts mixed keys arrays and not all numerically indexed arrays are packed
1011-
if (UNEXPECTED(HT_IS_PACKED(Z_ARRVAL_P(class_map_zv)))) {
1022+
if (UNEXPECTED(!soap_class_map_has_only_string_keys(Z_ARRVAL_P(class_map_zv)))) {
10121023
zend_argument_value_error(2, "\"classmap\" option must be an associative array");
10131024
goto cleanup;
10141025
}
@@ -2104,6 +2115,20 @@ PHP_METHOD(SoapClient, __construct)
21042115
RETURN_THROWS();
21052116
}
21062117

2118+
if (options != NULL) {
2119+
zval *classmap = zend_hash_str_find(Z_ARRVAL_P(options), "classmap", sizeof("classmap")-1);
2120+
if (classmap != NULL) {
2121+
if (UNEXPECTED(Z_TYPE_P(classmap) != IS_ARRAY)) {
2122+
zend_argument_type_error(2, "\"classmap\" option must be of type array, %s given", zend_zval_type_name(classmap));
2123+
RETURN_THROWS();
2124+
}
2125+
if (UNEXPECTED(!soap_class_map_has_only_string_keys(Z_ARRVAL_P(classmap)))) {
2126+
zend_argument_value_error(2, "\"classmap\" option must be an associative array");
2127+
RETURN_THROWS();
2128+
}
2129+
}
2130+
}
2131+
21072132
SOAP_CLIENT_BEGIN_CODE();
21082133

21092134
cache_wsdl = SOAP_GLOBAL(cache_enabled) ? SOAP_GLOBAL(cache_mode) : 0;
@@ -2134,14 +2159,6 @@ PHP_METHOD(SoapClient, __construct)
21342159
}
21352160
}
21362161

2137-
if ((tmp = zend_hash_str_find(ht, "stream_context", sizeof("stream_context")-1)) != NULL &&
2138-
Z_TYPE_P(tmp) == IS_RESOURCE) {
2139-
context = php_stream_context_from_zval(tmp, 1);
2140-
Z_ADDREF_P(tmp);
2141-
} else {
2142-
context = php_stream_context_alloc();
2143-
}
2144-
21452162
if ((tmp = zend_hash_str_find(ht, "location", sizeof("location")-1)) != NULL &&
21462163
Z_TYPE_P(tmp) == IS_STRING) {
21472164
ZVAL_STR_COPY(Z_CLIENT_LOCATION_P(this_ptr), Z_STR_P(tmp));
@@ -2187,17 +2204,6 @@ PHP_METHOD(SoapClient, __construct)
21872204
}
21882205
}
21892206
}
2190-
if ((tmp = zend_hash_str_find(ht, "local_cert", sizeof("local_cert")-1)) != NULL &&
2191-
Z_TYPE_P(tmp) == IS_STRING) {
2192-
if (!context) {
2193-
context = php_stream_context_alloc();
2194-
}
2195-
php_stream_context_set_option(context, "ssl", "local_cert", tmp);
2196-
if ((tmp = zend_hash_str_find(ht, "passphrase", sizeof("passphrase")-1)) != NULL &&
2197-
Z_TYPE_P(tmp) == IS_STRING) {
2198-
php_stream_context_set_option(context, "ssl", "passphrase", tmp);
2199-
}
2200-
}
22012207
if ((tmp = zend_hash_find(ht, ZSTR_KNOWN(ZEND_STR_TRACE))) != NULL &&
22022208
(Z_TYPE_P(tmp) == IS_TRUE ||
22032209
(Z_TYPE_P(tmp) == IS_LONG && Z_LVAL_P(tmp) == 1))) {
@@ -2233,12 +2239,29 @@ PHP_METHOD(SoapClient, __construct)
22332239
}
22342240
if ((tmp = zend_hash_str_find(ht, "classmap", sizeof("classmap")-1)) != NULL &&
22352241
Z_TYPE_P(tmp) == IS_ARRAY) {
2236-
if (UNEXPECTED(HT_IS_PACKED(Z_ARRVAL_P(tmp)))) {
2237-
php_error_docref(NULL, E_ERROR, "'classmap' option must be an associative array");
2238-
}
22392242
ZVAL_COPY(Z_CLIENT_CLASSMAP_P(this_ptr), tmp);
22402243
}
22412244

2245+
if ((tmp = zend_hash_str_find(ht, "stream_context", sizeof("stream_context")-1)) != NULL &&
2246+
Z_TYPE_P(tmp) == IS_RESOURCE) {
2247+
context = php_stream_context_from_zval(tmp, 1);
2248+
Z_ADDREF_P(tmp);
2249+
} else {
2250+
context = php_stream_context_alloc();
2251+
}
2252+
2253+
if ((tmp = zend_hash_str_find(ht, "local_cert", sizeof("local_cert")-1)) != NULL &&
2254+
Z_TYPE_P(tmp) == IS_STRING) {
2255+
if (!context) {
2256+
context = php_stream_context_alloc();
2257+
}
2258+
php_stream_context_set_option(context, "ssl", "local_cert", tmp);
2259+
if ((tmp = zend_hash_str_find(ht, "passphrase", sizeof("passphrase")-1)) != NULL &&
2260+
Z_TYPE_P(tmp) == IS_STRING) {
2261+
php_stream_context_set_option(context, "ssl", "passphrase", tmp);
2262+
}
2263+
}
2264+
22422265
if ((tmp = zend_hash_str_find(ht, "typemap", sizeof("typemap")-1)) != NULL &&
22432266
Z_TYPE_P(tmp) == IS_ARRAY &&
22442267
zend_hash_num_elements(Z_ARRVAL_P(tmp)) > 0) {
@@ -2313,6 +2336,7 @@ PHP_METHOD(SoapClient, __construct)
23132336
if (typemap_ht) {
23142337
soap_client_object_fetch(Z_OBJ_P(this_ptr))->typemap = soap_create_typemap(sdl, typemap_ht);
23152338
}
2339+
23162340
SOAP_CLIENT_END_CODE();
23172341
}
23182342
/* }}} */

ext/soap/tests/bugs/gh16256.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ try {
2020
}
2121
?>
2222
--EXPECT--
23-
SoapClient::__construct(): 'classmap' option must be an associative array
23+
SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
2424
SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
SoapClient and SoapServer classmap options must only contain string keys
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
8+
$emptyHash = ['type' => 'stdClass'];
9+
unset($emptyHash['type']);
10+
11+
$cases = [
12+
'empty' => [],
13+
'empty hash' => $emptyHash,
14+
'packed' => ['stdClass'],
15+
'sparse numeric' => [100 => 'stdClass'],
16+
'numeric string' => ['1' => 'stdClass'],
17+
'mixed' => ['type' => 'stdClass', 1 => 'stdClass'],
18+
'associative' => ['type' => 'stdClass'],
19+
];
20+
21+
foreach ($cases as $name => $classmap) {
22+
echo "-- $name --\n";
23+
24+
try {
25+
new SoapClient(null, [
26+
'location' => 'http://example.com/',
27+
'uri' => 'urn:test',
28+
'classmap' => $classmap,
29+
]);
30+
echo "SoapClient: OK\n";
31+
} catch (Throwable $e) {
32+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
33+
}
34+
35+
try {
36+
new SoapServer(null, [
37+
'uri' => 'urn:test',
38+
'classmap' => $classmap,
39+
]);
40+
echo "SoapServer: OK\n";
41+
} catch (Throwable $e) {
42+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
43+
}
44+
}
45+
46+
?>
47+
--EXPECT--
48+
-- empty --
49+
SoapClient: OK
50+
SoapServer: OK
51+
-- empty hash --
52+
SoapClient: OK
53+
SoapServer: OK
54+
-- packed --
55+
ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
56+
ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
57+
-- sparse numeric --
58+
ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
59+
ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
60+
-- numeric string --
61+
ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
62+
ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
63+
-- mixed --
64+
ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
65+
ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
66+
-- associative --
67+
SoapClient: OK
68+
SoapServer: OK
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
SoapClient and SoapServer report an invalid classmap option as ValueError
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
8+
$classmap = ['type' => 'stdClass', 1 => 'stdClass'];
9+
10+
try {
11+
new SoapClient(null, [
12+
'location' => 'http://example.com/',
13+
'uri' => 'urn:test',
14+
'classmap' => $classmap,
15+
]);
16+
} catch (Throwable $e) {
17+
echo 'SoapClient: ', $e::class, ': ', $e->getMessage(), PHP_EOL;
18+
}
19+
20+
try {
21+
new SoapServer(null, [
22+
'uri' => 'urn:test',
23+
'classmap' => $classmap,
24+
]);
25+
} catch (Throwable $e) {
26+
echo 'SoapServer: ', $e::class, ': ', $e->getMessage(), PHP_EOL;
27+
}
28+
29+
?>
30+
--EXPECT--
31+
SoapClient: ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
32+
SoapServer: ValueError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be an associative array
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
SoapClient reports an invalid classmap option as ValueError when exceptions are disabled
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
8+
try {
9+
new SoapClient(null, [
10+
'location' => 'http://example.com/',
11+
'uri' => 'urn:test',
12+
'exceptions' => false,
13+
'classmap' => ['type' => 'stdClass', 1 => 'stdClass'],
14+
]);
15+
} catch (Throwable $e) {
16+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
17+
}
18+
19+
?>
20+
--EXPECT--
21+
ValueError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be an associative array
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
SoapClient and SoapServer classmap options must be arrays
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
8+
try {
9+
new SoapClient(null, [
10+
'location' => 'http://example.com/',
11+
'uri' => 'urn:test',
12+
'classmap' => 1,
13+
]);
14+
} catch (Throwable $e) {
15+
echo 'SoapClient: ', $e::class, ': ', $e->getMessage(), PHP_EOL;
16+
}
17+
18+
try {
19+
new SoapServer(null, [
20+
'uri' => 'urn:test',
21+
'classmap' => 1,
22+
]);
23+
} catch (Throwable $e) {
24+
echo 'SoapServer: ', $e::class, ': ', $e->getMessage(), PHP_EOL;
25+
}
26+
27+
?>
28+
--EXPECT--
29+
SoapClient: TypeError: SoapClient::__construct(): Argument #2 ($options) "classmap" option must be of type array, int given
30+
SoapServer: TypeError: SoapServer::__construct(): Argument #2 ($options) "classmap" option must be of type array, int given
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
SoapClient must not read packed private classmap as string-keyed map
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
8+
class LocalSoapClient extends SoapClient {
9+
public function __doRequest($request, $location, $action, $version, $one_way = false, ?string $uriParserClass = null): string {
10+
echo "__doRequest called\n";
11+
12+
return <<<'XML'
13+
<?xml version="1.0" encoding="UTF-8"?>
14+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
15+
<SOAP-ENV:Body>
16+
<SOAP-ENV:Fault>
17+
<faultcode>SOAP-ENV:Server</faultcode>
18+
<faultstring>expected fault</faultstring>
19+
</SOAP-ENV:Fault>
20+
</SOAP-ENV:Body>
21+
</SOAP-ENV:Envelope>
22+
XML;
23+
}
24+
}
25+
26+
class Foo {}
27+
28+
$client = new LocalSoapClient(null, [
29+
'location' => 'http://example.org/',
30+
'uri' => 'http://example.org/',
31+
]);
32+
33+
$property = new ReflectionProperty(SoapClient::class, '_classmap');
34+
$property->setValue($client, ['Foo']);
35+
36+
try {
37+
$client->__soapCall('foo', [new Foo()]);
38+
} catch (SoapFault) {
39+
echo "SOAP Fault thrown\n";
40+
}
41+
42+
?>
43+
--EXPECT--
44+
__doRequest called
45+
SOAP Fault thrown

0 commit comments

Comments
 (0)