Commit 04f3d282f8d for php.net

commit 04f3d282f8d090ed1c1dd9fb42144cea2b110beb
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Jul 16 08:05:16 2026 -0400

    ext/standard: reject a dechunk chunk size that overflows size_t

    php_dechunk() accumulated the hex chunk size with chunk_size * 16 + digit and
    never checked the multiply, so a size of 2^64 wrapped to 0. A zero size reads
    as the terminating chunk, so the filter stopped there and dropped the body it
    had been handed. Error out instead, as the other malformed-size cases already
    do.

    Closes GH-22786

diff --git a/ext/standard/filters.c b/ext/standard/filters.c
index a7c0a035a23..0c08600648c 100644
--- a/ext/standard/filters.c
+++ b/ext/standard/filters.c
@@ -1715,12 +1715,14 @@ static size_t php_dechunk(char *buf, size_t len, php_chunked_filter_data *data)
 				data->chunk_size = 0;
 			case CHUNK_SIZE:
 				while (p < end) {
+					size_t digit;
+
 					if (*p >= '0' && *p <= '9') {
-						data->chunk_size = (data->chunk_size * 16) + (*p - '0');
+						digit = *p - '0';
 					} else if (*p >= 'A' && *p <= 'F') {
-						data->chunk_size = (data->chunk_size * 16) + (*p - 'A' + 10);
+						digit = *p - 'A' + 10;
 					} else if (*p >= 'a' && *p <= 'f') {
-						data->chunk_size = (data->chunk_size * 16) + (*p - 'a' + 10);
+						digit = *p - 'a' + 10;
 					} else if (data->state == CHUNK_SIZE_START) {
 						data->state = CHUNK_ERROR;
 						break;
@@ -1728,6 +1730,11 @@ static size_t php_dechunk(char *buf, size_t len, php_chunked_filter_data *data)
 						data->state = CHUNK_SIZE_EXT;
 						break;
 					}
+					if (data->chunk_size > (SIZE_MAX / 16)) {
+						data->state = CHUNK_ERROR;
+						break;
+					}
+					data->chunk_size = (data->chunk_size * 16) + digit;
 					data->state = CHUNK_SIZE;
 					p++;
 				}
diff --git a/ext/standard/tests/filters/dechunk_size_overflow.phpt b/ext/standard/tests/filters/dechunk_size_overflow.phpt
new file mode 100644
index 00000000000..ac42733e16e
--- /dev/null
+++ b/ext/standard/tests/filters/dechunk_size_overflow.phpt
@@ -0,0 +1,38 @@
+--TEST--
+dechunk filter must reject a chunk size that overflows size_t
+--SKIPIF--
+<?php
+$filters = stream_get_filters();
+if(! in_array( "dechunk", $filters )) die( "skip Chunked filter not available." );
+?>
+--INI--
+allow_url_fopen=1
+--FILE--
+<?php
+/* Both sizes exceed SIZE_MAX, so parsing stops and the rest is passed through
+   raw. The guard trips at SIZE_MAX/16, so how many digits are consumed first
+   follows the width of size_t: %s covers the leftover run. Unguarded, the
+   first size wraps to 0, which reads as the terminating chunk and drops the
+   body; the second wraps to SIZE_MAX and swallows the rest as one chunk. */
+$streams = [
+    "data://text/plain,10000000000000000\nBODYDATA\n0\n",
+    "data://text/plain,fffffffffffffffff\nBODYDATA\n0\n",
+    "data://text/plain,5\nhello\n0\n",
+];
+foreach ($streams as $name) {
+    $fp = fopen($name, "r");
+    stream_filter_append($fp, "dechunk", STREAM_FILTER_READ);
+    var_dump(stream_get_contents($fp));
+    fclose($fp);
+}
+?>
+--EXPECTF--
+string(%d) "%s
+BODYDATA
+0
+"
+string(%d) "%s
+BODYDATA
+0
+"
+string(5) "hello"