Skip to content

Commit 4767061

Browse files
authored
Fix bug #69280: SoapClient classmap doesn't support fully qualified class name (#14398)
There's a hash table that maps type names to class name, but names with a leading backslash are not supported. The engine has logic to strip away the leading backslash that we should replicate here. It works by checking if we need to make an actual copy in case an unexpected (e.g. invalid data or leading backslash) situations are detected. Upon making a copy we normalize the data in the table. Furthermore, previously the code assumed that the key was always valid and that the structure was a non-packed hash table. This isn't necessarily the case. The new code fixes this as well. Closes GH-14398.
1 parent be7f3aa commit 4767061

File tree

7 files changed

+146
-14
lines changed

7 files changed

+146
-14
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ PHP NEWS
3535
. Fix memory leak if calling SoapServer::setClass() twice. (nielsdos)
3636
. Fix reading zlib ini settings in ext-soap. (nielsdos)
3737
. Fix memory leaks with string function name lookups. (nielsdos)
38+
. Fixed bug #69280 (SoapClient classmap doesn't support fully qualified class
39+
name). (nielsdos)
3840

3941
- Sodium:
4042
. Fix memory leaks in ext/sodium on failure of some functions. (nielsdos)

ext/soap/php_encoding.c

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -449,11 +449,8 @@ static xmlNodePtr master_to_xml_int(encodePtr encode, zval *data, int style, xml
449449
zend_string *type_name;
450450

451451
ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(SOAP_GLOBAL(class_map), type_name, tmp) {
452-
ZVAL_DEREF(tmp);
453-
if (Z_TYPE_P(tmp) == IS_STRING &&
454-
ZSTR_LEN(ce->name) == Z_STRLEN_P(tmp) &&
455-
zend_binary_strncasecmp(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), Z_STRVAL_P(tmp), ZSTR_LEN(ce->name), ZSTR_LEN(ce->name)) == 0 &&
456-
type_name) {
452+
if (ZSTR_LEN(ce->name) == Z_STRLEN_P(tmp) &&
453+
zend_binary_strncasecmp(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), Z_STRVAL_P(tmp), ZSTR_LEN(ce->name), ZSTR_LEN(ce->name)) == 0) {
457454

458455
/* TODO: namespace isn't stored */
459456
encodePtr enc = NULL;
@@ -1382,7 +1379,6 @@ static zval *to_zval_object_ex(zval *ret, encodeTypePtr type, xmlNodePtr data, z
13821379
zend_class_entry *tmp;
13831380

13841381
if ((classname = zend_hash_str_find_deref(SOAP_GLOBAL(class_map), type->type_str, strlen(type->type_str))) != NULL &&
1385-
Z_TYPE_P(classname) == IS_STRING &&
13861382
(tmp = zend_fetch_class(Z_STR_P(classname), ZEND_FETCH_CLASS_AUTO)) != NULL) {
13871383
ce = tmp;
13881384
}
@@ -3629,3 +3625,48 @@ void delete_encoder_persistent(zval *zv)
36293625
assert(t->details.map == NULL);
36303626
free(t);
36313627
}
3628+
3629+
/* Normalize leading backslash similarly to how the engine strips it away. */
3630+
static inline zend_string *drop_leading_backslash(zend_string *str) {
3631+
if (ZSTR_VAL(str)[0] == '\\') {
3632+
return zend_string_init(ZSTR_VAL(str) + 1, ZSTR_LEN(str) - 1, false);
3633+
} else {
3634+
return zend_string_copy(str);
3635+
}
3636+
}
3637+
3638+
static HashTable *create_normalized_classmap_copy(HashTable *class_map)
3639+
{
3640+
HashTable *normalized = zend_new_array(zend_hash_num_elements(class_map));
3641+
3642+
zend_string *key;
3643+
zval *value;
3644+
ZEND_HASH_FOREACH_STR_KEY_VAL(class_map, key, value) {
3645+
ZVAL_DEREF(value);
3646+
3647+
if (key != NULL && Z_TYPE_P(value) == IS_STRING) {
3648+
zval zv;
3649+
ZVAL_STR(&zv, drop_leading_backslash(Z_STR_P(value)));
3650+
zend_hash_add_new(normalized, key, &zv);
3651+
}
3652+
} ZEND_HASH_FOREACH_END();
3653+
3654+
return normalized;
3655+
}
3656+
3657+
void create_normalized_classmap(zval *return_value, zval *class_map)
3658+
{
3659+
/* Check if we need to make a copy. */
3660+
zend_string *key;
3661+
zval *value;
3662+
ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARR_P(class_map), key, value) {
3663+
if (key == NULL || Z_TYPE_P(value) != IS_STRING || ZSTR_VAL(Z_STR_P(value))[0] == '\\') {
3664+
/* TODO: should probably throw in some of these cases to indicate programmer error,
3665+
* e.g. in the case where a non-string (after dereferencing) is provided. */
3666+
RETURN_ARR(create_normalized_classmap_copy(Z_ARR_P(class_map)));
3667+
}
3668+
} ZEND_HASH_FOREACH_END();
3669+
3670+
/* We didn't have to make an actual copy, just increment the refcount. */
3671+
RETURN_COPY(class_map);
3672+
}

ext/soap/php_encoding.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ encodePtr get_conversion(int encode);
214214
void delete_encoder(zval *zv);
215215
void delete_encoder_persistent(zval *zv);
216216

217+
void create_normalized_classmap(zval *return_value, zval *class_map);
218+
217219
extern const encode defaultEncoding[];
218220
extern int numDefaultEncodings;
219221

ext/soap/php_soap.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ struct _soapService {
9898
char *actor;
9999
char *uri;
100100
xmlCharEncodingHandlerPtr encoding;
101-
HashTable *class_map;
101+
zval class_map;
102102
int features;
103103
struct _soapHeader **soap_headers_ptr;
104104
int send_errors;

ext/soap/soap.c

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ PHP_METHOD(SoapServer, __construct)
816816

817817
if ((tmp = zend_hash_str_find(ht, "classmap", sizeof("classmap")-1)) != NULL &&
818818
Z_TYPE_P(tmp) == IS_ARRAY) {
819-
service->class_map = zend_array_dup(Z_ARRVAL_P(tmp));
819+
create_normalized_classmap(&service->class_map, tmp);
820820
}
821821

822822
if ((tmp = zend_hash_str_find(ht, "typemap", sizeof("typemap")-1)) != NULL &&
@@ -1282,7 +1282,7 @@ PHP_METHOD(SoapServer, handle)
12821282
old_encoding = SOAP_GLOBAL(encoding);
12831283
SOAP_GLOBAL(encoding) = service->encoding;
12841284
old_class_map = SOAP_GLOBAL(class_map);
1285-
SOAP_GLOBAL(class_map) = service->class_map;
1285+
SOAP_GLOBAL(class_map) = Z_ARR(service->class_map);
12861286
old_typemap = SOAP_GLOBAL(typemap);
12871287
SOAP_GLOBAL(typemap) = service->typemap;
12881288
old_features = SOAP_GLOBAL(features);
@@ -1982,7 +1982,7 @@ PHP_METHOD(SoapClient, __construct)
19821982
}
19831983
if ((tmp = zend_hash_str_find(ht, "classmap", sizeof("classmap")-1)) != NULL &&
19841984
Z_TYPE_P(tmp) == IS_ARRAY) {
1985-
ZVAL_COPY(Z_CLIENT_CLASSMAP_P(this_ptr), tmp);
1985+
create_normalized_classmap(Z_CLIENT_CLASSMAP_P(this_ptr), tmp);
19861986
}
19871987

19881988
if ((tmp = zend_hash_str_find(ht, "typemap", sizeof("typemap")-1)) != NULL &&
@@ -4384,10 +4384,7 @@ static void delete_service(void *data) /* {{{ */
43844384
if (service->encoding) {
43854385
xmlCharEncCloseFunc(service->encoding);
43864386
}
4387-
if (service->class_map) {
4388-
zend_hash_destroy(service->class_map);
4389-
FREE_HASHTABLE(service->class_map);
4390-
}
4387+
zval_ptr_dtor(&service->class_map);
43914388
zval_ptr_dtor(&service->soap_object);
43924389
efree(service);
43934390
}

ext/soap/tests/bug69280.phpt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Bug #69280 (SoapClient classmap doesn't support fully qualified class name)
3+
--EXTENSIONS--
4+
soap
5+
--INI--
6+
soap.wsdl_cache_enabled=0
7+
--CREDITS--
8+
champetier dot etienne at gmail dot com
9+
--FILE--
10+
<?php
11+
abstract class AbstractClass {
12+
public $prop;
13+
}
14+
15+
class RealClass1 extends AbstractClass {
16+
public $prop1;
17+
}
18+
19+
class TestWS extends \SoapClient {
20+
public function TestMethod($parameters) {
21+
return $this->__soapCall('TestMethod', [$parameters], [
22+
'uri' => 'http://tempuri.org/',
23+
'soapaction' => ''
24+
]
25+
);
26+
}
27+
28+
public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = false): ?string {
29+
die($request);
30+
}
31+
}
32+
33+
$a = new TestWS(__DIR__ . '/bug69280.wsdl', ['classmap' => [
34+
'AbstractClass' => '\AbstractClass',
35+
'RealClass1' => '\RealClass1',
36+
]]);
37+
$r1 = new \RealClass1();
38+
$r1->prop = "prop";
39+
$r1->prop1 = "prop1";
40+
$a->TestMethod($r1);
41+
?>
42+
--EXPECT--
43+
<?xml version="1.0" encoding="UTF-8"?>
44+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tempuri.org/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><SOAP-ENV:Body><parameters xsi:type="ns1:RealClass1"><ns1:prop>prop</ns1:prop><ns1:prop1>prop1</ns1:prop1></parameters></SOAP-ENV:Body></SOAP-ENV:Envelope>

ext/soap/tests/bug69280.wsdl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:s0="http://tempuri.org/" name="TestWS" targetNamespace="http://tempuri.org/" xmlns="http://schemas.xmlsoap.org/wsdl/">
3+
<types>
4+
<xs:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/" xmlns:xs="http://www.w3.org/2001/XMLSchema">
5+
<xs:complexType name="AbstractClass">
6+
<xs:sequence>
7+
<xs:element minOccurs="0" maxOccurs="1" name="prop" type="xs:string" />
8+
</xs:sequence>
9+
</xs:complexType>
10+
<xs:complexType name="RealClass1">
11+
<xs:complexContent mixed="false">
12+
<xs:extension base="s0:AbstractClass">
13+
<xs:sequence>
14+
<xs:element minOccurs="0" maxOccurs="1" name="prop1" type="xs:string" />
15+
</xs:sequence>
16+
</xs:extension>
17+
</xs:complexContent>
18+
</xs:complexType>
19+
</xs:schema>
20+
</types>
21+
<message name="TestMethodSoapIn">
22+
<part name="parameters" element="RealClass1" />
23+
</message>
24+
<portType name="TestWSSoap">
25+
<operation name="TestMethod">
26+
<input message="s0:TestMethodSoapIn" />
27+
</operation>
28+
</portType>
29+
<binding name="TestWSSoap" type="s0:TestWSSoap">
30+
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
31+
<operation name="TestMethod">
32+
<soap:operation soapAction="http://tempuri.org/TestMethod" style="document" />
33+
<input>
34+
<soap:body use="literal" />
35+
</input>
36+
<output>
37+
<soap:body use="literal" />
38+
</output>
39+
</operation>
40+
</binding>
41+
<service name="TestWS">
42+
<port name="TestWSSoap" binding="s0:TestWSSoap">
43+
<soap:address location="http://tempuri.org/" />
44+
</port>
45+
</service>
46+
</definitions>

0 commit comments

Comments
 (0)