Commit 998bce0f0fe for php.net

commit 998bce0f0fe517bc8fbd7a4699111a8ff8f02846
Author: Weilin Du <weilindu@php.net>
Date:   Fri Jun 26 14:42:49 2026 +0800

    Fix GH-14695: Strictly validate invalid upload_max_filesize and post_max_size values

diff --git a/NEWS b/NEWS
index 9df461452c1..5f1d3b95173 100644
--- a/NEWS
+++ b/NEWS
@@ -32,6 +32,8 @@ PHP                                                                        NEWS
     surrounding property access). (timwolla)
   . Fixed GH-22422 (zend_arena layout mismatch leaked memory in separately
     built extensions under AddressSanitizer). (iliaal)
+  . Added validation for invalid upload_max_filesize and post_max_size values
+    (GH-14695). (Weilin Du)
   . TSRM: use local-exec TLS in PIE executables. (henderkes)
   . perf: make all static extensions use TSRMG_STATIC. (henderkes)
   . Fixed bug GH-22257 (type confusion in Exception::getTraceAsString()).
diff --git a/Zend/tests/zend_ini/gh14695.phpt b/Zend/tests/zend_ini/gh14695.phpt
new file mode 100644
index 00000000000..a682c16eff3
--- /dev/null
+++ b/Zend/tests/zend_ini/gh14695.phpt
@@ -0,0 +1,17 @@
+--TEST--
+GH-14695: Invalid upload_max_filesize and post_max_size values are rejected
+--INI--
+upload_max_filesize=1zz
+post_max_size=bogus
+--FILE--
+<?php
+
+var_dump(ini_get('upload_max_filesize'));
+var_dump(ini_get('post_max_size'));
+
+?>
+--EXPECTF--
+Warning: Invalid "upload_max_filesize" setting. Invalid quantity "1zz": unknown multiplier "z" in %s on line %d
+Warning: Invalid "post_max_size" setting. Invalid quantity "bogus": no valid leading digits in %s on line %d
+string(2) "2M"
+string(2) "8M"
diff --git a/Zend/tests/zend_ini/gh14695_empty.phpt b/Zend/tests/zend_ini/gh14695_empty.phpt
new file mode 100644
index 00000000000..260644122df
--- /dev/null
+++ b/Zend/tests/zend_ini/gh14695_empty.phpt
@@ -0,0 +1,13 @@
+--TEST--
+GH-14695: Empty upload_max_filesize value is rejected
+--INI--
+upload_max_filesize=
+--FILE--
+<?php
+
+var_dump(ini_get('upload_max_filesize'));
+
+?>
+--EXPECTF--
+Warning: Invalid "upload_max_filesize" setting. Invalid quantity "": no valid leading digits in %s on line %d
+string(2) "2M"
diff --git a/Zend/zend_ini.c b/Zend/zend_ini.c
index 093683526d3..78a1974a92e 100644
--- a/Zend/zend_ini.c
+++ b/Zend/zend_ini.c
@@ -614,7 +614,12 @@ static const char *zend_ini_consume_quantity_prefix(const char *const digits, co
 	return digits_consumed;
 }

-static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zend_ini_parse_quantity_signed_result_t signed_result, zend_string **errstr) /* {{{ */
+static zend_ulong zend_ini_parse_quantity_internal(
+	const zend_string *value,
+	zend_ini_parse_quantity_signed_result_t signed_result,
+	zend_string **errstr,
+	bool strict
+) /* {{{ */
 {
 	char *digits_end = NULL;
 	const char *str = ZSTR_VAL(value);
@@ -634,6 +639,18 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 	while (digits < str_end && zend_is_whitespace(*(str_end-1))) {--str_end;}

 	if (digits == str_end) {
+		if (strict) {
+			if (ZSTR_LEN(value) == 0) {
+				*errstr = zend_strpprintf(0, "Invalid quantity \"\": no valid leading digits");
+			} else {
+				smart_str_append_escaped(&invalid, ZSTR_VAL(value), ZSTR_LEN(value));
+				smart_str_0(&invalid);
+				*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits",
+								ZSTR_VAL(invalid.s));
+				smart_str_free(&invalid);
+			}
+			return 0;
+		}
 		*errstr = NULL;
 		return 0;
 	}
@@ -653,8 +670,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 		smart_str_append_escaped(&invalid, ZSTR_VAL(value), ZSTR_LEN(value));
 		smart_str_0(&invalid);

-		*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits, interpreting as \"0\" for backwards compatibility",
-						ZSTR_VAL(invalid.s));
+		if (strict) {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits",
+							ZSTR_VAL(invalid.s));
+		} else {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits, interpreting as \"0\" for backwards compatibility",
+							ZSTR_VAL(invalid.s));
+		}

 		smart_str_free(&invalid);
 		return 0;
@@ -690,8 +712,12 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 				base = 2;
 				break;
 			default:
-				*errstr = zend_strpprintf(0, "Invalid prefix \"0%c\", interpreting as \"0\" for backwards compatibility",
-					digits[1]);
+				if (strict) {
+					*errstr = zend_strpprintf(0, "Invalid prefix \"0%c\"", digits[1]);
+				} else {
+					*errstr = zend_strpprintf(0, "Invalid prefix \"0%c\", interpreting as \"0\" for backwards compatibility",
+						digits[1]);
+				}
 				return 0;
         }
         digits += 2;
@@ -702,8 +728,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 			smart_str_append_escaped(&invalid, ZSTR_VAL(value), ZSTR_LEN(value));
 			smart_str_0(&invalid);

-			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no digits after base prefix, interpreting as \"0\" for backwards compatibility",
-							ZSTR_VAL(invalid.s));
+			if (strict) {
+				*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no digits after base prefix",
+								ZSTR_VAL(invalid.s));
+			} else {
+				*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no digits after base prefix, interpreting as \"0\" for backwards compatibility",
+								ZSTR_VAL(invalid.s));
+			}

 			smart_str_free(&invalid);
 			return 0;
@@ -744,8 +775,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 		smart_str_append_escaped(&invalid, ZSTR_VAL(value), ZSTR_LEN(value));
 		smart_str_0(&invalid);

-		*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits, interpreting as \"0\" for backwards compatibility",
-						ZSTR_VAL(invalid.s));
+		if (strict) {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits",
+							ZSTR_VAL(invalid.s));
+		} else {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": no valid leading digits, interpreting as \"0\" for backwards compatibility",
+							ZSTR_VAL(invalid.s));
+		}

 		smart_str_free(&invalid);
 		return 0;
@@ -781,8 +817,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 			smart_str_append_escaped(&chr, str_end-1, 1);
 			smart_str_0(&chr);

-			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": unknown multiplier \"%s\", interpreting as \"%s\" for backwards compatibility",
-						ZSTR_VAL(invalid.s), ZSTR_VAL(chr.s), ZSTR_VAL(interpreted.s));
+			if (strict) {
+				*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": unknown multiplier \"%s\"",
+							ZSTR_VAL(invalid.s), ZSTR_VAL(chr.s));
+			} else {
+				*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": unknown multiplier \"%s\", interpreting as \"%s\" for backwards compatibility",
+							ZSTR_VAL(invalid.s), ZSTR_VAL(chr.s), ZSTR_VAL(interpreted.s));
+			}

 			smart_str_free(&invalid);
 			smart_str_free(&interpreted);
@@ -815,8 +856,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 		smart_str_append_escaped(&chr, str_end-1, 1);
 		smart_str_0(&chr);

-		*errstr = zend_strpprintf(0, "Invalid quantity \"%s\", interpreting as \"%s%s\" for backwards compatibility",
-						ZSTR_VAL(invalid.s), ZSTR_VAL(interpreted.s), ZSTR_VAL(chr.s));
+		if (strict) {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": invalid characters before multiplier \"%s\"",
+							ZSTR_VAL(invalid.s), ZSTR_VAL(chr.s));
+		} else {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\", interpreting as \"%s%s\" for backwards compatibility",
+							ZSTR_VAL(invalid.s), ZSTR_VAL(interpreted.s), ZSTR_VAL(chr.s));
+		}

 		smart_str_free(&invalid);
 		smart_str_free(&interpreted);
@@ -833,8 +879,13 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen
 		/* Not specifying the resulting value here because the caller may make
 		 * additional conversions. Not specifying the allowed range
 		 * because the caller may do narrower range checks. */
-		*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": value is out of range, using overflow result for backwards compatibility",
-						ZSTR_VAL(invalid.s));
+		if (strict) {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": value is out of range",
+							ZSTR_VAL(invalid.s));
+		} else {
+			*errstr = zend_strpprintf(0, "Invalid quantity \"%s\": value is out of range, using overflow result for backwards compatibility",
+							ZSTR_VAL(invalid.s));
+		}

 		smart_str_free(&invalid);
 		smart_str_free(&interpreted);
@@ -850,13 +901,25 @@ static zend_ulong zend_ini_parse_quantity_internal(const zend_string *value, zen

 ZEND_API zend_long zend_ini_parse_quantity(const zend_string *value, zend_string **errstr) /* {{{ */
 {
-	return (zend_long) zend_ini_parse_quantity_internal(value, ZEND_INI_PARSE_QUANTITY_SIGNED, errstr);
+	return (zend_long) zend_ini_parse_quantity_internal(value, ZEND_INI_PARSE_QUANTITY_SIGNED, errstr, false);
 }
 /* }}} */

 ZEND_API zend_ulong zend_ini_parse_uquantity(const zend_string *value, zend_string **errstr) /* {{{ */
 {
-	return zend_ini_parse_quantity_internal(value, ZEND_INI_PARSE_QUANTITY_UNSIGNED, errstr);
+	return zend_ini_parse_quantity_internal(value, ZEND_INI_PARSE_QUANTITY_UNSIGNED, errstr, false);
+}
+/* }}} */
+
+ZEND_API zend_result zend_ini_parse_quantity_strict(const zend_string *value, zend_long *result, zend_string **errstr) /* {{{ */
+{
+	zend_long retval = (zend_long) zend_ini_parse_quantity_internal(value, ZEND_INI_PARSE_QUANTITY_SIGNED, errstr, true);
+	if (*errstr) {
+		return FAILURE;
+	}
+
+	*result = retval;
+	return SUCCESS;
 }
 /* }}} */

@@ -974,6 +1037,23 @@ ZEND_API ZEND_INI_MH(OnUpdateLong) /* {{{ */
 }
 /* }}} */

+ZEND_API ZEND_INI_MH(OnUpdateLongStrict) /* {{{ */
+{
+	zend_long tmp;
+	zend_string *errstr;
+	if (UNEXPECTED(zend_ini_parse_quantity_strict(new_value, &tmp, &errstr) == FAILURE)) {
+		zend_error(E_WARNING, "Invalid \"%s\" setting. %s", ZSTR_VAL(entry->name), ZSTR_VAL(errstr));
+		zend_string_release(errstr);
+		return FAILURE;
+	}
+
+	zend_long *p = ZEND_INI_GET_ADDR();
+	*p = tmp;
+
+	return SUCCESS;
+}
+/* }}} */
+
 ZEND_API ZEND_INI_MH(OnUpdateLongGEZero) /* {{{ */
 {
 	zend_long tmp = zend_ini_parse_quantity_warn(new_value, entry->name);
diff --git a/Zend/zend_ini.h b/Zend/zend_ini.h
index dbe650675b6..a181048ad9a 100644
--- a/Zend/zend_ini.h
+++ b/Zend/zend_ini.h
@@ -142,6 +142,12 @@ ZEND_API zend_long zend_ini_parse_quantity(const zend_string *value, zend_string
  */
 ZEND_API zend_ulong zend_ini_parse_uquantity(const zend_string *value, zend_string **errstr);

+/**
+ * Strict variant of zend_ini_parse_quantity. Ill-formatted values fail instead
+ * of returning a backwards-compatible interpretation.
+ */
+ZEND_API zend_result zend_ini_parse_quantity_strict(const zend_string *value, zend_long *result, zend_string **errstr);
+
 ZEND_API zend_long zend_ini_parse_quantity_warn(const zend_string *value, zend_string *setting);

 ZEND_API zend_ulong zend_ini_parse_uquantity_warn(const zend_string *value, zend_string *setting);
@@ -207,6 +213,7 @@ END_EXTERN_C()
 BEGIN_EXTERN_C()
 ZEND_API ZEND_INI_MH(OnUpdateBool);
 ZEND_API ZEND_INI_MH(OnUpdateLong);
+ZEND_API ZEND_INI_MH(OnUpdateLongStrict);
 ZEND_API ZEND_INI_MH(OnUpdateLongGEZero);
 ZEND_API ZEND_INI_MH(OnUpdateReal);
 /* char* versions */
diff --git a/ext/standard/http.c b/ext/standard/http.c
index d65e7a8acaa..667eddaf0be 100644
--- a/ext/standard/http.c
+++ b/ext/standard/http.c
@@ -247,17 +247,17 @@ PHP_FUNCTION(http_build_query)
 }
 /* }}} */

-static zend_result cache_request_parse_body_option(HashTable *options, zval *option, int cache_offset)
+static zend_result cache_request_parse_body_option(zval *option, const char *option_name, int cache_offset)
 {
 	if (option) {
 		zend_long result;
 		ZVAL_DEREF(option);
 		if (Z_TYPE_P(option) == IS_STRING) {
 			zend_string *errstr;
-			result = zend_ini_parse_quantity(Z_STR_P(option), &errstr);
-			if (errstr) {
-				zend_error(E_WARNING, "%s", ZSTR_VAL(errstr));
+			if (UNEXPECTED(zend_ini_parse_quantity_strict(Z_STR_P(option), &result, &errstr) == FAILURE)) {
+				zend_value_error("Invalid \"%s\" value in $options argument: %s", option_name, ZSTR_VAL(errstr));
 				zend_string_release(errstr);
+				return FAILURE;
 			}
 		} else if (Z_TYPE_P(option) == IS_LONG) {
 			result = Z_LVAL_P(option);
@@ -290,7 +290,7 @@ static zend_result cache_request_parse_body_options(HashTable *options)

 #define CHECK_OPTION(name) \
 	if (zend_string_equals_literal_ci(key, #name)) { \
-		if (cache_request_parse_body_option(options, value, REQUEST_PARSE_BODY_OPTION_ ## name) == FAILURE) { \
+		if (cache_request_parse_body_option(value, #name, REQUEST_PARSE_BODY_OPTION_ ## name) == FAILURE) { \
 			return FAILURE; \
 		} \
 		continue; \
diff --git a/ext/standard/tests/http/request_parse_body/multipart_options_invalid_quantity.phpt b/ext/standard/tests/http/request_parse_body/multipart_options_invalid_quantity.phpt
index b1efa0dbc91..63514f597b5 100644
--- a/ext/standard/tests/http/request_parse_body/multipart_options_invalid_quantity.phpt
+++ b/ext/standard/tests/http/request_parse_body/multipart_options_invalid_quantity.phpt
@@ -3,15 +3,17 @@
 --FILE--
 <?php

-try {
-    request_parse_body(options: [
-        'upload_max_filesize' => '1GB',
-    ]);
-} catch (Throwable $e) {
-    echo get_class($e), ': ', $e->getMessage(), "\n";
+foreach (['1GB', ''] as $value) {
+    try {
+        request_parse_body(options: [
+            'upload_max_filesize' => $value,
+        ]);
+    } catch (Throwable $e) {
+        echo get_class($e), ': ', $e->getMessage(), "\n";
+    }
 }

 ?>
 --EXPECTF--
-Warning: Invalid quantity "1GB": unknown multiplier "B", interpreting as "1" for backwards compatibility in %s on line %d
-RequestParseBodyException: Request does not provide a content type
+ValueError: Invalid "upload_max_filesize" value in $options argument: Invalid quantity "1GB": unknown multiplier "B"
+ValueError: Invalid "upload_max_filesize" value in $options argument: Invalid quantity "": no valid leading digits
diff --git a/main/main.c b/main/main.c
index 6bda55ac874..15bed5be988 100644
--- a/main/main.c
+++ b/main/main.c
@@ -845,8 +845,8 @@ PHP_INI_BEGIN()
 	STD_PHP_INI_ENTRY("open_basedir",			NULL,		PHP_INI_ALL,		OnUpdateBaseDir,			open_basedir,			php_core_globals,	core_globals)

 	STD_PHP_INI_BOOLEAN("file_uploads",			"1",		PHP_INI_SYSTEM,		OnUpdateBool,			file_uploads,			php_core_globals,	core_globals)
-	STD_PHP_INI_ENTRY("upload_max_filesize",	"2M",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLong,			upload_max_filesize,	php_core_globals,	core_globals)
-	STD_PHP_INI_ENTRY("post_max_size",			"8M",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLong,			post_max_size,			sapi_globals_struct,sapi_globals)
+	STD_PHP_INI_ENTRY("upload_max_filesize",	"2M",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLongStrict,		upload_max_filesize,	php_core_globals,	core_globals)
+	STD_PHP_INI_ENTRY("post_max_size",			"8M",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLongStrict,		post_max_size,			sapi_globals_struct,sapi_globals)
 	STD_PHP_INI_ENTRY("upload_tmp_dir",			NULL,		PHP_INI_SYSTEM,		OnUpdateStringUnempty,	upload_tmp_dir,			php_core_globals,	core_globals)
 	STD_PHP_INI_ENTRY("max_input_nesting_level", "64",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLongGEZero,	max_input_nesting_level,			php_core_globals,	core_globals)
 	STD_PHP_INI_ENTRY("max_input_vars",			"1000",		PHP_INI_SYSTEM|PHP_INI_PERDIR,		OnUpdateLongGEZero,	max_input_vars,						php_core_globals,	core_globals)