Commit f983e1aabf3 for php

commit f983e1aabf36cdb02d47a57dd5072f777acdba47
Author: Kévin Dunglas <kevin@dunglas.dev>
Date:   Tue Nov 10 17:09:10 2026 +0100

    Fix uploaded files leak in request_parse_body() (#25387)

    Calling request_parse_body() on a multipart body that was already parsed,
    either at startup with enable_post_data_reading=1 or by a previous call,
    replaces SG(rfc1867_uploaded_files). Earlier uploads lose their upload
    status and their temporary files are not deleted at request shutdown.

    Reuse the existing uploaded-files table so is_uploaded_file() and
    move_uploaded_file() continue to recognize earlier uploads and request
    shutdown can clean them up. Cover both startup parsing and repeated calls,
    including moving the uploaded files.

    Co-authored-by: David Carlier <devnexen@gmail.com>

    Closes #25387

diff --git a/NEWS b/NEWS
index 4299e2d5fe2..9fbe37c9d27 100644
--- a/NEWS
+++ b/NEWS
@@ -97,6 +97,8 @@ PHP                                                                        NEWS
 - Standard:
   . Fixed dns_get_record() returning partially built records for malformed
     DNS responses. (Marti2203)
+  . Fixed bug GH-25387 (Uploaded files leak in request_parse_body()).
+    (Kévin Dunglas)

 - Streams:
   . Fixed bug GH-22063 (Stream filter chain UAF on self-removal during
diff --git a/ext/standard/tests/http/request_parse_body/multipart_after_startup_parse.phpt b/ext/standard/tests/http/request_parse_body/multipart_after_startup_parse.phpt
new file mode 100644
index 00000000000..0b3976ad692
--- /dev/null
+++ b/ext/standard/tests/http/request_parse_body/multipart_after_startup_parse.phpt
@@ -0,0 +1,42 @@
+--TEST--
+request_parse_body() keeps files uploaded during startup parsing
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332
+-----------------------------84000087610663814162942123332
+Content-Disposition: form-data; name="file_name"; filename="original_file_name.txt"
+Content-Type: text/plain
+
+file data
+-----------------------------84000087610663814162942123332--
+--FILE--
+<?php
+
+$tmp_name = $_FILES['file_name']['tmp_name'];
+var_dump(is_uploaded_file($tmp_name));
+
+var_dump(request_parse_body());
+
+var_dump(is_uploaded_file($tmp_name));
+
+$dst = __DIR__ . '/multipart_after_startup_parse.txt';
+var_dump(move_uploaded_file($tmp_name, $dst));
+var_dump(file_get_contents($dst));
+
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/multipart_after_startup_parse.txt');
+?>
+--EXPECT--
+bool(true)
+array(2) {
+  [0]=>
+  array(0) {
+  }
+  [1]=>
+  array(0) {
+  }
+}
+bool(true)
+bool(true)
+string(9) "file data"
diff --git a/ext/standard/tests/http/request_parse_body/multipart_twice.phpt b/ext/standard/tests/http/request_parse_body/multipart_twice.phpt
new file mode 100644
index 00000000000..27b6e58f94b
--- /dev/null
+++ b/ext/standard/tests/http/request_parse_body/multipart_twice.phpt
@@ -0,0 +1,45 @@
+--TEST--
+request_parse_body() called twice keeps files uploaded by the first call
+--ENV--
+REQUEST_METHOD=PUT
+--POST_RAW--
+Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332
+-----------------------------84000087610663814162942123332
+Content-Disposition: form-data; name="file_name"; filename="original_file_name.txt"
+Content-Type: text/plain
+
+file data
+-----------------------------84000087610663814162942123332--
+--FILE--
+<?php
+
+[, $files] = request_parse_body();
+$tmp_name = $files['file_name']['tmp_name'];
+var_dump(is_uploaded_file($tmp_name));
+
+var_dump(request_parse_body());
+
+var_dump(is_uploaded_file($tmp_name));
+
+$dst = __DIR__ . '/multipart_twice.txt';
+var_dump(move_uploaded_file($tmp_name, $dst));
+var_dump(file_get_contents($dst));
+
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/multipart_twice.txt');
+?>
+--EXPECT--
+bool(true)
+array(2) {
+  [0]=>
+  array(0) {
+  }
+  [1]=>
+  array(0) {
+  }
+}
+bool(true)
+bool(true)
+string(9) "file data"
diff --git a/main/rfc1867.c b/main/rfc1867.c
index 39733e52c47..e7c228a79be 100644
--- a/main/rfc1867.c
+++ b/main/rfc1867.c
@@ -694,7 +694,6 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
 	size_t array_len = 0;
 	int64_t total_bytes = 0, max_file_size = 0;
 	int skip_upload = 0, anonymous_index = 0;
-	HashTable *uploaded_files = NULL;
 	multipart_buffer *mbuff;
 	zval *array_ptr = (zval *) arg;
 	bool throw_exceptions = SG(request_parse_body_context).throw_exceptions;
@@ -792,9 +791,11 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
 	/* Initialize $_FILES[] */
 	zend_hash_init(&PG(rfc1867_protected_variables), 8, NULL, NULL, 0);

-	ALLOC_HASHTABLE(uploaded_files);
-	zend_hash_init(uploaded_files, 8, NULL, free_filename, 0);
-	SG(rfc1867_uploaded_files) = uploaded_files;
+	/* request_parse_body() may parse the body again, keep tracking earlier uploads */
+	if (!SG(rfc1867_uploaded_files)) {
+		ALLOC_HASHTABLE(SG(rfc1867_uploaded_files));
+		zend_hash_init(SG(rfc1867_uploaded_files), 8, NULL, free_filename, 0);
+	}

 	if (Z_TYPE(PG(http_globals)[TRACK_VARS_FILES]) != IS_ARRAY) {
 		/* php_auto_globals_create_files() might have already done that */