Commit 21c2318e295 for php.net

commit 21c2318e2952098a421e7fdff70c8fab9262686c
Author: Weilin Du <108666168+LamentXU123@users.noreply.github.com>
Date:   Thu Jun 4 18:30:24 2026 +0800

    Reject out-of-range SOAP schema integers (#22178)

    Fix the bug where SOAP schema parsing silently accepts out-of-range XML Schema integer values, see #22167

diff --git a/NEWS b/NEWS
index 5fc78a65515..8e5da57950a 100644
--- a/NEWS
+++ b/NEWS
@@ -178,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 543e1996e1b..35f8276adc3 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -120,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/soap/php_schema.c b/ext/soap/php_schema.c
index 73d6691af26..d97a0eac05b 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 00000000000..f24bfb0eac3
--- /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--
+<?php
+function wsdl_with_schema(string $schema): string {
+    return <<<XML
+<?xml version="1.0"?>
+<definitions
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:tns="http://test-uri/"
+    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+    xmlns="http://schemas.xmlsoap.org/wsdl/"
+    targetNamespace="http://test-uri/">
+  <types>
+    <xsd:schema targetNamespace="http://test-uri/">
+      $schema
+    </xsd:schema>
+  </types>
+  <message name="m"><part name="p" type="tns:T"/></message>
+  <portType name="p"><operation name="op"><input message="tns:m"/></operation></portType>
+  <binding name="b" type="tns:p">
+    <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
+    <operation name="op">
+      <soap:operation soapAction="#op"/>
+      <input>
+        <soap:body use="encoded"
+                   namespace="http://test-uri/"
+                   encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
+      </input>
+    </operation>
+  </binding>
+  <service name="s"><port name="p" binding="tns:b"><soap:address location="test://"/></port></service>
+</definitions>
+XML;
+}
+
+function occurrence_schema(string $attribute, string $value = "2147483648"): string {
+    return <<<XML
+<xsd:complexType name="T">
+  <xsd:sequence>
+    <xsd:element name="x" type="xsd:string" $attribute="$value"/>
+  </xsd:sequence>
+</xsd:complexType>
+XML;
+}
+
+function restriction_schema(string $facet, string $value = "2147483648"): string {
+    return <<<XML
+<xsd:simpleType name="T">
+  <xsd:restriction base="xsd:int">
+    <xsd:$facet value="$value"/>
+  </xsd:restriction>
+</xsd:simpleType>
+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