diff --git a/NEWS b/NEWS index 3c4d6993f46c..8e5da57950a7 100644 --- a/NEWS +++ b/NEWS @@ -73,6 +73,8 @@ PHP NEWS types. (Weilin Du) . Fixed MessageFormatter::parse() and parseMessage() returning PHP_INT_MIN as float rather than int on 64-bit platforms. (Weilin Du) + . Fixed UConverter::transcode() silently truncating from_subst and to_subst + option lengths greater than 127 bytes. (Weilin Du) - JSON: . Enriched JSON last error / exception message with error location. @@ -176,6 +178,9 @@ PHP NEWS represented as a string anymore but a int. (David Carlier) . Fixed bug GH-21421 (SoapClient typemap property breaks engine assumptions). (ndossche) + . WSDL/XML Schema parsing now rejects out-of-range integer values for + occurrence constraints and integer restriction facets. Negative minOccurs + and maxOccurs values are rejected as well. (Weilin Du) - Sockets: . Added the TCP_USER_TIMEOUT constant for Linux to set the maximum time in diff --git a/UPGRADING b/UPGRADING index 95299dd5117f..35f8276adc39 100644 --- a/UPGRADING +++ b/UPGRADING @@ -44,6 +44,9 @@ PHP 8.6 UPGRADE NOTES int, rather than float, on 64-bit platforms when parsing integer values. . The $type parameter of IntlBreakIterator::getPartsIterator() has been changed from string to int to match the underlying implementation. + . UConverter::transcode() now rejects from_subst and to_subst option values + longer than 127 bytes instead of silently truncating the length before + passing it to ICU. - PCNTL: . pcntl_alarm() now raises a ValueError if the seconds argument is @@ -117,6 +120,11 @@ PHP 8.6 UPGRADE NOTES to 1). RFC: https://wiki.php.net/rfc/session_security_defaults +- SOAP: + . WSDL/XML Schema parsing now rejects out-of-range integer values for + occurrence constraints and integer restriction facets. Negative minOccurs + and maxOccurs values are rejected as well. + - SPL: . SplObjectStorage::getHash() implementations may no longer mutate any SplObjectStorage instance. Attempting to do so now throws an Error. diff --git a/ext/intl/converter/converter.cpp b/ext/intl/converter/converter.cpp index 597746b8faca..88f05136bbbd 100644 --- a/ext/intl/converter/converter.cpp +++ b/ext/intl/converter/converter.cpp @@ -682,6 +682,16 @@ static zend_string* php_converter_do_convert(UConverter *dest_cnv, } /* }}} */ +static void php_converter_set_subst_chars(UConverter *cnv, zend_string *subst, UErrorCode *error) +{ + if (ZSTR_LEN(subst) > SCHAR_MAX) { + *error = U_ILLEGAL_ARGUMENT_ERROR; + return; + } + + ucnv_setSubstChars(cnv, ZSTR_VAL(subst), (int8_t) ZSTR_LEN(subst), error); +} + /* {{{ */ #define UCNV_REASON_CASE(v) case (UCNV_ ## v) : RETURN_STRINGL( "REASON_" #v , sizeof( "REASON_" #v ) - 1); PHP_METHOD(UConverter, reasonText) { @@ -761,13 +771,13 @@ PHP_METHOD(UConverter, transcode) { (tmpzval = zend_hash_str_find_deref(Z_ARRVAL_P(options), "from_subst", sizeof("from_subst") - 1)) != NULL && Z_TYPE_P(tmpzval) == IS_STRING) { error = U_ZERO_ERROR; - ucnv_setSubstChars(src_cnv, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval) & 0x7F, &error); + php_converter_set_subst_chars(src_cnv, Z_STR_P(tmpzval), &error); } if (U_SUCCESS(error) && (tmpzval = zend_hash_str_find_deref(Z_ARRVAL_P(options), "to_subst", sizeof("to_subst") - 1)) != NULL && Z_TYPE_P(tmpzval) == IS_STRING) { error = U_ZERO_ERROR; - ucnv_setSubstChars(dest_cnv, Z_STRVAL_P(tmpzval), Z_STRLEN_P(tmpzval) & 0x7F, &error); + php_converter_set_subst_chars(dest_cnv, Z_STR_P(tmpzval), &error); } } diff --git a/ext/intl/rangeformatter/rangeformatter_class.cpp b/ext/intl/rangeformatter/rangeformatter_class.cpp index 6921791e588f..2df252d1986b 100644 --- a/ext/intl/rangeformatter/rangeformatter_class.cpp +++ b/ext/intl/rangeformatter/rangeformatter_class.cpp @@ -60,6 +60,19 @@ zend_object *IntlNumberRangeFormatter_object_create(zend_class_entry *ce) return &intern->zo; } +static icu::Formattable rangeformatter_create_formattable(zval *number) +{ + icu::Formattable formattable; + + if (Z_TYPE_P(number) == IS_DOUBLE) { + formattable.setDouble(Z_DVAL_P(number)); + } else { + formattable.setInt64(static_cast(Z_LVAL_P(number))); + } + + return formattable; +} + U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, __construct) { ZEND_PARSE_PARAMETERS_NONE(); @@ -154,8 +167,8 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, format) UErrorCode error = U_ZERO_ERROR; - icu::Formattable start_formattable(Z_TYPE_P(start) == IS_DOUBLE ? Z_DVAL_P(start) : Z_LVAL_P(start)); - icu::Formattable end_formattable(Z_TYPE_P(end) == IS_DOUBLE ? Z_DVAL_P(end) : Z_LVAL_P(end)); + icu::Formattable start_formattable = rangeformatter_create_formattable(start); + icu::Formattable end_formattable = rangeformatter_create_formattable(end); UnicodeString result = RANGEFORMATTER_OBJECT(obj)->formatFormattableRange(start_formattable, end_formattable, error).toString(error); diff --git a/ext/intl/tests/rangeformatter/rangeformatter_format_int64.phpt b/ext/intl/tests/rangeformatter/rangeformatter_format_int64.phpt new file mode 100644 index 000000000000..9cefbcfb1ea8 --- /dev/null +++ b/ext/intl/tests/rangeformatter/rangeformatter_format_int64.phpt @@ -0,0 +1,27 @@ +--TEST-- +IntlNumberRangeFormatter::format() preserves int64 precision +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format(9007199254740993, 9007199254740993); +var_dump(preg_replace('/[^0-9]/', '', $formatted)); +?> +--EXPECT-- +string(16) "9007199254740993" diff --git a/ext/intl/tests/uconverter_transcode_subst_length.phpt b/ext/intl/tests/uconverter_transcode_subst_length.phpt new file mode 100644 index 000000000000..93001b16680c --- /dev/null +++ b/ext/intl/tests/uconverter_transcode_subst_length.phpt @@ -0,0 +1,20 @@ +--TEST-- +UConverter::transcode() rejects too long substitution strings +--EXTENSIONS-- +intl +--INI-- +intl.use_exceptions=false +--FILE-- + $subst])); +echo intl_get_error_message(), "\n"; +var_dump(UConverter::transcode('abc', 'UTF-8', 'ASCII', ['to_subst' => $subst])); +echo intl_get_error_message(), "\n"; +?> +--EXPECT-- +bool(false) +UConverter::transcode(): returned error 1: U_ILLEGAL_ARGUMENT_ERROR: U_ILLEGAL_ARGUMENT_ERROR +bool(false) +UConverter::transcode(): returned error 1: U_ILLEGAL_ARGUMENT_ERROR: U_ILLEGAL_ARGUMENT_ERROR diff --git a/ext/soap/php_schema.c b/ext/soap/php_schema.c index 73d6691af262..d97a0eac05b5 100644 --- a/ext/soap/php_schema.c +++ b/ext/soap/php_schema.c @@ -53,6 +53,28 @@ static bool node_is_equal_xsd(xmlNodePtr node, const char *name) return node_is_equal_ex_one_of(node, name, ns); } +static int schema_parse_int(const xmlChar *value, const char *name, bool allow_negative) +{ + const char *str = (const char *) value; + zend_long lval = 0; + int oflow_info = 0; + uint8_t type = is_numeric_string_ex(str, strlen(str), &lval, NULL, true, &oflow_info, NULL); + + if (type != IS_LONG) { + errno = 0; + lval = ZEND_STRTOL(str, NULL, 10); + if (oflow_info || (errno == ERANGE && lval != 0)) { + soap_error1(E_ERROR, "Parsing Schema: %s value is out of range", name); + } + } + + if (ZEND_LONG_EXCEEDS_INT(lval) || (!allow_negative && lval < 0)) { + soap_error1(E_ERROR, "Parsing Schema: %s value is out of range", name); + } + + return (int) lval; +} + static encodePtr create_encoder(sdlPtr sdl, sdlTypePtr cur_type, const xmlChar *ns, const xmlChar *type) { smart_str nscat = {0}; @@ -854,7 +876,7 @@ static int schema_restriction_var_int(xmlNodePtr val, sdlRestrictionIntPtr *valp soap_error0(E_ERROR, "Parsing Schema: missing restriction value"); } - (*valptr)->value = atoi((char*)value->children->content); + (*valptr)->value = schema_parse_int(value->children->content, (const char *) val->name, true); return TRUE; } @@ -1016,7 +1038,7 @@ void schema_min_max(xmlNodePtr node, sdlContentModelPtr model) xmlAttrPtr attr = get_attribute(node->properties, "minOccurs"); if (attr) { - model->min_occurs = atoi((char*)attr->children->content); + model->min_occurs = schema_parse_int(attr->children->content, "minOccurs", false); } else { model->min_occurs = 1; } @@ -1026,7 +1048,7 @@ void schema_min_max(xmlNodePtr node, sdlContentModelPtr model) if (!strncmp((char*)attr->children->content, "unbounded", sizeof("unbounded"))) { model->max_occurs = -1; } else { - model->max_occurs = atoi((char*)attr->children->content); + model->max_occurs = schema_parse_int(attr->children->content, "maxOccurs", false); } } else { model->max_occurs = 1; diff --git a/ext/soap/tests/bugs/gh22167.phpt b/ext/soap/tests/bugs/gh22167.phpt new file mode 100644 index 000000000000..f24bfb0eac32 --- /dev/null +++ b/ext/soap/tests/bugs/gh22167.phpt @@ -0,0 +1,128 @@ +--TEST-- +GH-22167 (Out-of-range XML Schema integer values in SOAP WSDL) +--EXTENSIONS-- +soap +--INI-- +soap.wsdl_cache_enabled=0 +--FILE-- + + + + + $schema + + + + + + + + + + + + + + + +XML; +} + +function occurrence_schema(string $attribute, string $value = "2147483648"): string { + return << + + + + +XML; +} + +function restriction_schema(string $facet, string $value = "2147483648"): string { + return << + + + + +XML; +} + +$cases = [ + "minOccurs" => occurrence_schema("minOccurs"), + "maxOccurs" => occurrence_schema("maxOccurs"), + "negative minOccurs" => occurrence_schema("minOccurs", "-1"), + "negative maxOccurs" => occurrence_schema("maxOccurs", "-1"), + "minExclusive" => restriction_schema("minExclusive"), + "minInclusive" => restriction_schema("minInclusive"), + "maxExclusive" => restriction_schema("maxExclusive"), + "maxInclusive" => restriction_schema("maxInclusive"), + "totalDigits" => restriction_schema("totalDigits"), + "fractionDigits" => restriction_schema("fractionDigits"), + "length" => restriction_schema("length"), + "minLength" => restriction_schema("minLength"), + "maxLength" => restriction_schema("maxLength"), +]; + +$numeric_string_cases = [ + "leading whitespace numeric-string" => " 2147483648", + "leading plus numeric-string" => "+2147483648", + "leading zero numeric-string" => "00000000002147483648", + "leading numeric-string with trailing data" => "2147483648abc", + "negative out-of-range numeric-string" => "-2147483649", + "decimal numeric-string" => "2147483648.0", + "exponent numeric-string" => "2147483648e0", +]; + +foreach ($numeric_string_cases as $name => $value) { + $cases[$name] = occurrence_schema("maxOccurs", $value); +} + +$cases["fractional numeric-string within int range"] = occurrence_schema("maxOccurs", "3.141"); + +foreach ($cases as $name => $schema) { + $file = tempnam(sys_get_temp_dir(), "wsdl"); + file_put_contents($file, wsdl_with_schema($schema)); + + try { + new SoapClient($file, ["cache_wsdl" => WSDL_CACHE_NONE]); + echo "$name: parsed\n"; + } catch (SoapFault $e) { + echo "$name: {$e->getMessage()}\n"; + } finally { + unlink($file); + } +} +?> +--EXPECT-- +minOccurs: SOAP-ERROR: Parsing Schema: minOccurs value is out of range +maxOccurs: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +negative minOccurs: SOAP-ERROR: Parsing Schema: minOccurs value is out of range +negative maxOccurs: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +minExclusive: SOAP-ERROR: Parsing Schema: minExclusive value is out of range +minInclusive: SOAP-ERROR: Parsing Schema: minInclusive value is out of range +maxExclusive: SOAP-ERROR: Parsing Schema: maxExclusive value is out of range +maxInclusive: SOAP-ERROR: Parsing Schema: maxInclusive value is out of range +totalDigits: SOAP-ERROR: Parsing Schema: totalDigits value is out of range +fractionDigits: SOAP-ERROR: Parsing Schema: fractionDigits value is out of range +length: SOAP-ERROR: Parsing Schema: length value is out of range +minLength: SOAP-ERROR: Parsing Schema: minLength value is out of range +maxLength: SOAP-ERROR: Parsing Schema: maxLength value is out of range +leading whitespace numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +leading plus numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +leading zero numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +leading numeric-string with trailing data: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +negative out-of-range numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +decimal numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +exponent numeric-string: SOAP-ERROR: Parsing Schema: maxOccurs value is out of range +fractional numeric-string within int range: parsed