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 */