From bc13a4ba2413b5aa4390ebcc57e7d19bb31fe9c5 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Sat, 17 Jun 2023 22:26:21 +0200 Subject: [PATCH 1/9] Add request_parse_body() function This function allows populating the $_POST and $_FILES globals for non-post requests. This avoids manual parsing of RFC1867 requests. Fixes #55815 --- Zend/Optimizer/zend_func_infos.h | 1 + .../request_parse_body/invalid_boundary.phpt | 25 ++++ .../request_parse_body/multipart_garbled.phpt | 32 +++++ .../multipart_max_files.phpt | 37 ++++++ .../multipart_max_input_vars.phpt | 35 +++++ .../multipart_max_parts.phpt | 36 +++++ .../multipart_missing_boundary.phpt | 27 ++++ .../multipart_options_invalid_key.phpt | 21 +++ .../multipart_options_invalid_value_type.phpt | 16 +++ .../multipart_options_max_files.phpt | 39 ++++++ .../multipart_options_max_input_vars.phpt | 37 ++++++ .../multipart_options_max_parts.phpt | 38 ++++++ .../multipart_options_post_max_size.phpt | 37 ++++++ ...multipart_options_upload_max_filesize.phpt | 47 +++++++ .../request_parse_body/multipart_stream.phpt | 56 ++++++++ .../unsupported_content_type.phpt | 27 ++++ .../request_parse_body/urlencoded_stream.phpt | 34 +++++ ext/mbstring/mb_gpc.c | 5 +- ext/standard/basic_functions.stub.php | 7 + ext/standard/basic_functions_arginfo.h | 8 +- ext/standard/html.c | 123 ++++++++++++++++++ main/SAPI.c | 16 ++- main/SAPI.h | 22 ++++ main/php_variables.c | 8 +- main/rfc1867.c | 59 +++++---- run-tests.php | 4 +- sapi/cli/php_cli_server.c | 4 + ...-54hq-v5wp-fqgv-max-body-parts-custom.phpt | 2 +- ...54hq-v5wp-fqgv-max-body-parts-default.phpt | 4 +- .../tests/request_parse_body_multipart.phpt | 97 ++++++++++++++ .../tests/request_parse_body_urlencoded.phpt | 75 +++++++++++ sapi/fpm/tests/tester.inc | 8 +- 32 files changed, 944 insertions(+), 43 deletions(-) create mode 100644 Zend/tests/request_parse_body/invalid_boundary.phpt create mode 100644 Zend/tests/request_parse_body/multipart_garbled.phpt create mode 100644 Zend/tests/request_parse_body/multipart_max_files.phpt create mode 100644 Zend/tests/request_parse_body/multipart_max_input_vars.phpt create mode 100644 Zend/tests/request_parse_body/multipart_max_parts.phpt create mode 100644 Zend/tests/request_parse_body/multipart_missing_boundary.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_invalid_key.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_max_files.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_max_parts.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_post_max_size.phpt create mode 100644 Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt create mode 100644 Zend/tests/request_parse_body/multipart_stream.phpt create mode 100644 Zend/tests/request_parse_body/unsupported_content_type.phpt create mode 100644 Zend/tests/request_parse_body/urlencoded_stream.phpt create mode 100644 sapi/fpm/tests/request_parse_body_multipart.phpt create mode 100644 sapi/fpm/tests/request_parse_body_urlencoded.phpt diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 902a3269c9d3..c3b8589ac004 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -525,6 +525,7 @@ static const func_info_t func_infos[] = { F1("htmlspecialchars", MAY_BE_STRING), F1("htmlentities", MAY_BE_STRING), F1("get_html_translation_table", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), + F1("request_parse_body", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ARRAY), F1("bin2hex", MAY_BE_STRING), F1("hex2bin", MAY_BE_STRING|MAY_BE_FALSE), #if defined(HAVE_NL_LANGINFO) diff --git a/Zend/tests/request_parse_body/invalid_boundary.phpt b/Zend/tests/request_parse_body/invalid_boundary.phpt new file mode 100644 index 000000000000..4d7dba3751e6 --- /dev/null +++ b/Zend/tests/request_parse_body/invalid_boundary.phpt @@ -0,0 +1,25 @@ +--TEST-- +request_parse_body() with multipart and invalid boundary +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary="foobar +empty +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Invalid boundary in multipart/form-data POST data +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_garbled.phpt b/Zend/tests/request_parse_body/multipart_garbled.phpt new file mode 100644 index 000000000000..ee308de51994 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_garbled.phpt @@ -0,0 +1,32 @@ +--TEST-- +request_parse_body() with multipart and garbled field +--INI-- +max_file_uploads=1 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; +Content-Type: text/plain + +post field data +-----------------------------84000087610663814162942123332-- +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +File Upload Mime headers garbled +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_max_files.phpt b/Zend/tests/request_parse_body/multipart_max_files.phpt new file mode 100644 index 000000000000..cef11763d9bb --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_max_files.phpt @@ -0,0 +1,37 @@ +--TEST-- +request_parse_body() with multipart and exceeding max files +--INI-- +max_file_uploads=1 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file1"; filename="file1.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file2"; filename="file2.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332-- +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Maximum number of allowable file uploads has been exceeded +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt new file mode 100644 index 000000000000..ce45bd46d83c --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt @@ -0,0 +1,35 @@ +--TEST-- +request_parse_body() with multipart and exceeding max input vars +--INI-- +max_input_vars=1 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field1" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field2" + +post field data +-----------------------------84000087610663814162942123332-- +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_max_parts.phpt b/Zend/tests/request_parse_body/multipart_max_parts.phpt new file mode 100644 index 000000000000..aa8445085e7d --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_max_parts.phpt @@ -0,0 +1,36 @@ +--TEST-- +request_parse_body() with multipart and exceeding max parts +--INI-- +max_multipart_body_parts=1 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="post_field_name" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file_name"; filename="original_file_name.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332-- +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt new file mode 100644 index 000000000000..860cbb2d0e41 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt @@ -0,0 +1,27 @@ +--TEST-- +request_parse_body() with multipart and missing boundary +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data +empty +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Missing boundary in multipart/form-data POST data +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt new file mode 100644 index 000000000000..0ec7d04070d5 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt @@ -0,0 +1,21 @@ +--TEST-- +request_parse_body() invalid key +--FILE-- + 1]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + request_parse_body(options: ['moo' => 1]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Invalid key "foo" in $options argument +Invalid key "moo" in $options argument diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt new file mode 100644 index 000000000000..7dba161d41bf --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt @@ -0,0 +1,16 @@ +--TEST-- +request_parse_body() invalid value type +--FILE-- + [], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Invalid array value in $options argument diff --git a/Zend/tests/request_parse_body/multipart_options_max_files.phpt b/Zend/tests/request_parse_body/multipart_options_max_files.phpt new file mode 100644 index 000000000000..c3bda1be8215 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_max_files.phpt @@ -0,0 +1,39 @@ +--TEST-- +request_parse_body() max_file_uploads option +--INI-- +max_file_uploads=10 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file1"; filename="file1.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file2"; filename="file2.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332-- +--FILE-- + 1, + ]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Maximum number of allowable file uploads has been exceeded +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt new file mode 100644 index 000000000000..25773de53ca1 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt @@ -0,0 +1,37 @@ +--TEST-- +request_parse_body() max_input_vars option +--INI-- +max_input_vars=10 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field1" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field2" + +post field data +-----------------------------84000087610663814162942123332-- +--FILE-- + 1, + ]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt new file mode 100644 index 000000000000..4cbb4265af68 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt @@ -0,0 +1,38 @@ +--TEST-- +request_parse_body() max_multipart_body_parts option +--INI-- +max_multipart_body_parts=10 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="post_field_name" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file_name"; filename="original_file_name.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332-- +--FILE-- + 1, + ]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt new file mode 100644 index 000000000000..7734b4135b4e --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt @@ -0,0 +1,37 @@ +--TEST-- +request_parse_body() post_max_size option +--INI-- +post_max_size=1M +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field1" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="field2" + +post file data +-----------------------------84000087610663814162942123332-- +--FILE-- + '302', + ]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +POST Content-Length of 303 bytes exceeds the limit of 302 bytes +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt new file mode 100644 index 000000000000..0e6cdde4150f --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt @@ -0,0 +1,47 @@ +--TEST-- +request_parse_body() upload_max_filesize option +--INI-- +upload_max_filesize=1M +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: name="file1"; filename="file1.txt" + +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-----------------------------84000087610663814162942123332-- +--FILE-- + '128', + ]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECT-- +array(0) { +} +array(1) { + ["file1"]=> + array(6) { + ["name"]=> + string(9) "file1.txt" + ["full_path"]=> + string(9) "file1.txt" + ["type"]=> + string(0) "" + ["tmp_name"]=> + string(0) "" + ["error"]=> + int(1) + ["size"]=> + int(0) + } +} diff --git a/Zend/tests/request_parse_body/multipart_stream.phpt b/Zend/tests/request_parse_body/multipart_stream.phpt new file mode 100644 index 000000000000..94bfd254e3b0 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_stream.phpt @@ -0,0 +1,56 @@ +--TEST-- +request_parse_body() with multipart stream +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: multipart/form-data; boundary=---------------------------84000087610663814162942123332 +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="post_field_name" + +post field data +-----------------------------84000087610663814162942123332 +Content-Disposition: form-data; name="file_name"; filename="original_file_name.txt" +Content-Type: text/plain + +file data +-----------------------------84000087610663814162942123332-- +--FILE-- + +--CLEAN-- + +--EXPECTF-- +array(1) { + ["post_field_name"]=> + string(15) "post field data" +} +array(1) { + ["file_name"]=> + array(6) { + ["name"]=> + string(22) "original_file_name.txt" + ["full_path"]=> + string(22) "original_file_name.txt" + ["type"]=> + string(10) "text/plain" + ["tmp_name"]=> + string(%d) "%s" + ["error"]=> + int(0) + ["size"]=> + int(9) + } +} +string(9) "file data" diff --git a/Zend/tests/request_parse_body/unsupported_content_type.phpt b/Zend/tests/request_parse_body/unsupported_content_type.phpt new file mode 100644 index 000000000000..289189be0ef1 --- /dev/null +++ b/Zend/tests/request_parse_body/unsupported_content_type.phpt @@ -0,0 +1,27 @@ +--TEST-- +request_parse_body() with multipart and unsupported Content-Type +--INI-- +max_input_vars=1 +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: application/json +{"hello": "world"} +--FILE-- +getMessage(), "\n"; +} + +var_dump($_POST, $_FILES); + +?> +--EXPECTF-- +Content-Type "application/json" is not supported +array(0) { +} +array(0) { +} diff --git a/Zend/tests/request_parse_body/urlencoded_stream.phpt b/Zend/tests/request_parse_body/urlencoded_stream.phpt new file mode 100644 index 000000000000..384e48de9a65 --- /dev/null +++ b/Zend/tests/request_parse_body/urlencoded_stream.phpt @@ -0,0 +1,34 @@ +--TEST-- +request_parse_body() with urlencoded stream +--ENV-- +REQUEST_METHOD=PUT +--POST_RAW-- +Content-Type: application/x-www-form-urlencoded +foo=foo&bar[]=1&bar[]=2 +--FILE-- + +--CLEAN-- + +--EXPECT-- +array(2) { + ["foo"]=> + string(3) "foo" + ["bar"]=> + array(2) { + [0]=> + string(1) "1" + [1]=> + string(1) "2" + } +} +array(0) { +} diff --git a/ext/mbstring/mb_gpc.c b/ext/mbstring/mb_gpc.c index 33fbd32edc66..8be70f921fd4 100644 --- a/ext/mbstring/mb_gpc.c +++ b/ext/mbstring/mb_gpc.c @@ -221,8 +221,9 @@ const mbfl_encoding *_php_mb_encoding_handler_ex(const php_mb_encoding_handler_i var = php_strtok_r(NULL, info->separator, &strtok_buf); } - if (ZEND_SIZE_T_GT_ZEND_LONG(n, (PG(max_input_vars) * 2))) { - php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", PG(max_input_vars)); + zend_long max_input_vars = REQUEST_PARSE_BODY_OPTION_GET(max_input_vars, PG(max_input_vars)); + if (ZEND_SIZE_T_GT_ZEND_LONG(n, max_input_vars * 2)) { + php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", max_input_vars); goto out; } diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 02396691e5b9..231c1e039f1e 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2289,6 +2289,13 @@ function htmlentities(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | */ function get_html_translation_table(int $table = HTML_SPECIALCHARS, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, string $encoding = "UTF-8"): array {} +/** + * @param array|null $options + * @return array + * @refcount 1 + */ +function request_parse_body(?array $options = null): array {} + /* }}} */ /* assert.c */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 40ce2e70d911..df67600642a3 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 7584d1aec417e84718a7a60e244cb00df2dc039f */ + * Stub hash: d45833d18fb6cd9db147418fac9bc22ce07bd3a9 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -792,6 +792,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_get_html_translation_table, 0, 0 ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 0, "\"UTF-8\"") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_request_parse_body, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_assert, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, assertion, IS_MIXED, 0) ZEND_ARG_OBJ_TYPE_MASK(0, description, Throwable, MAY_BE_STRING|MAY_BE_NULL, "null") @@ -2534,6 +2538,7 @@ ZEND_FUNCTION(htmlspecialchars_decode); ZEND_FUNCTION(html_entity_decode); ZEND_FUNCTION(htmlentities); ZEND_FUNCTION(get_html_translation_table); +ZEND_FUNCTION(request_parse_body); ZEND_FUNCTION(assert); ZEND_FUNCTION(assert_options); ZEND_FUNCTION(bin2hex); @@ -3167,6 +3172,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(html_entity_decode, arginfo_html_entity_decode) ZEND_FE(htmlentities, arginfo_htmlentities) ZEND_FE(get_html_translation_table, arginfo_get_html_translation_table) + ZEND_FE(request_parse_body, arginfo_request_parse_body) ZEND_FE(assert, arginfo_assert) ZEND_DEP_FE(assert_options, arginfo_assert_options) ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(bin2hex, arginfo_bin2hex) diff --git a/ext/standard/html.c b/ext/standard/html.c index 731ee340185b..a4e1940d70b7 100644 --- a/ext/standard/html.c +++ b/ext/standard/html.c @@ -50,6 +50,7 @@ #include #include "html_tables.h" +#include "zend_exceptions.h" /* Macro for disabling flag of translation of non-basic entities where this isn't supported. * Not appropriate for html_entity_decode/htmlspecialchars_decode */ @@ -1559,3 +1560,125 @@ PHP_FUNCTION(get_html_translation_table) } } /* }}} */ + +static zend_result cache_request_parse_body_option(HashTable *options, zval *option, int cache_offset) +{ + if (option) { + zend_long result; + 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)); + zend_string_release(errstr); + } + } else if (Z_TYPE_P(option) == IS_LONG) { + result = Z_LVAL_P(option); + } else { + zend_value_error("Invalid %s value in $options argument", zend_get_type_by_const(Z_TYPE_P(option))); + return FAILURE; + } + SG(request_parse_body_context).options_cache[cache_offset].set = true; + SG(request_parse_body_context).options_cache[cache_offset].value = result; + } else { + SG(request_parse_body_context).options_cache[cache_offset].set = false; + } + + return SUCCESS; +} + +static zend_result cache_request_parse_body_options(HashTable *options) +{ + zend_string *key; + zval *value; + ZEND_HASH_FOREACH_STR_KEY_VAL(options, key, value) { + if (!key) { + zend_value_error("Invalid integer key in $options argument"); + return FAILURE; + } + if (ZSTR_LEN(key) == 0) { + zend_value_error("Invalid empty string key in $options argument"); + return FAILURE; + } + +#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) { \ + return FAILURE; \ + } \ + continue; \ + } + + switch (ZSTR_VAL(key)[0]) { + case 'm': + case 'M': + CHECK_OPTION(max_file_uploads); + CHECK_OPTION(max_input_vars); + CHECK_OPTION(max_multipart_body_parts); + break; + case 'p': + case 'P': + CHECK_OPTION(post_max_size); + break; + case 'u': + case 'U': + CHECK_OPTION(upload_max_filesize); + break; + } + + zend_value_error("Invalid key \"%s\" in $options argument", ZSTR_VAL(key)); + return FAILURE; + } ZEND_HASH_FOREACH_END(); + +#undef CACHE_OPTION + + return SUCCESS; +} + +PHP_FUNCTION(request_parse_body) +{ + HashTable *options = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + SG(request_parse_body_context).throw_exceptions = true; + if (options) { + if (cache_request_parse_body_options(options) == FAILURE) { + goto exit; + } + } + + if (!SG(request_info).content_type) { + zend_throw_error(NULL, "Request does not provide a content type"); + goto exit; + } + + sapi_read_post_data(); + if (!SG(request_info).post_entry) { + zend_throw_exception_ex(NULL, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); + goto exit; + } + + zval post, files, old_post, old_files; + zval *global_post = &PG(http_globals)[TRACK_VARS_POST]; + zval *global_files = &PG(http_globals)[TRACK_VARS_FILES]; + + ZVAL_COPY_VALUE(&old_post, global_post); + ZVAL_COPY_VALUE(&old_files, global_files); + array_init(global_post); + array_init(global_files); + sapi_handle_post(global_post); + ZVAL_COPY_VALUE(&post, global_post); + ZVAL_COPY_VALUE(&files, global_files); + ZVAL_COPY_VALUE(global_post, &old_post); + ZVAL_COPY_VALUE(global_files, &old_files); + + RETVAL_ARR(zend_new_pair(&post, &files)); + +exit: + SG(request_parse_body_context).throw_exceptions = false; + memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); +} diff --git a/main/SAPI.c b/main/SAPI.c index e40570206378..e30a04144db4 100644 --- a/main/SAPI.c +++ b/main/SAPI.c @@ -169,7 +169,7 @@ SAPI_API void sapi_handle_post(void *arg) } } -static void sapi_read_post_data(void) +SAPI_API void sapi_read_post_data(void) { sapi_post_entry *post_entry; uint32_t content_type_length = (uint32_t)strlen(SG(request_info).content_type); @@ -255,9 +255,11 @@ SAPI_API size_t sapi_read_post_block(char *buffer, size_t buflen) SAPI_API SAPI_POST_READER_FUNC(sapi_read_standard_form_data) { - if ((SG(post_max_size) > 0) && (SG(request_info).content_length > SG(post_max_size))) { + zend_long post_max_size = REQUEST_PARSE_BODY_OPTION_GET(post_max_size, SG(post_max_size)); + + if (post_max_size > 0 && SG(request_info).content_length > post_max_size) { php_error_docref(NULL, E_WARNING, "POST Content-Length of " ZEND_LONG_FMT " bytes exceeds the limit of " ZEND_LONG_FMT " bytes", - SG(request_info).content_length, SG(post_max_size)); + SG(request_info).content_length, post_max_size); return; } @@ -281,8 +283,8 @@ SAPI_API SAPI_POST_READER_FUNC(sapi_read_standard_form_data) } } - if ((SG(post_max_size) > 0) && (SG(read_post_bytes) > SG(post_max_size))) { - php_error_docref(NULL, E_WARNING, "Actual POST length does not match Content-Length, and exceeds " ZEND_LONG_FMT " bytes", SG(post_max_size)); + if (post_max_size > 0 && SG(read_post_bytes) > post_max_size) { + php_error_docref(NULL, E_WARNING, "Actual POST length does not match Content-Length, and exceeds " ZEND_LONG_FMT " bytes", post_max_size); break; } @@ -455,6 +457,8 @@ SAPI_API void sapi_activate(void) SG(request_info).headers_only = 0; } SG(rfc1867_uploaded_files) = NULL; + SG(request_parse_body_context).throw_exceptions = false; + memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); /* Handle request method */ if (SG(server_context)) { @@ -1018,7 +1022,7 @@ SAPI_API zend_stat_t *sapi_get_stat(void) SAPI_API char *sapi_getenv(const char *name, size_t name_len) { char *value, *tmp; - + if (!sapi_module.getenv) { return NULL; } diff --git a/main/SAPI.h b/main/SAPI.h index 33c0e280d739..284f4cb96f1f 100644 --- a/main/SAPI.h +++ b/main/SAPI.h @@ -108,6 +108,26 @@ typedef struct { int proto_num; } sapi_request_info; +typedef struct { + bool throw_exceptions; + struct { + bool set; + zend_long value; + } options_cache[5]; +} sapi_request_parse_body_context; + +typedef enum { + REQUEST_PARSE_BODY_OPTION_max_file_uploads = 0, + REQUEST_PARSE_BODY_OPTION_max_input_vars, + REQUEST_PARSE_BODY_OPTION_max_multipart_body_parts, + REQUEST_PARSE_BODY_OPTION_post_max_size, + REQUEST_PARSE_BODY_OPTION_upload_max_filesize, +} request_parse_body_option; + +#define REQUEST_PARSE_BODY_OPTION_GET(name, fallback) \ + (SG(request_parse_body_context).options_cache[REQUEST_PARSE_BODY_OPTION_ ## name].set \ + ? SG(request_parse_body_context).options_cache[REQUEST_PARSE_BODY_OPTION_ ## name].value \ + : (fallback)) typedef struct _sapi_globals_struct { void *server_context; @@ -127,6 +147,7 @@ typedef struct _sapi_globals_struct { HashTable known_post_content_types; zval callback_func; zend_fcall_info_cache fci_cache; + sapi_request_parse_body_context request_parse_body_context; } sapi_globals_struct; @@ -186,6 +207,7 @@ SAPI_API int sapi_add_header_ex(const char *header_line, size_t header_line_len, SAPI_API int sapi_send_headers(void); SAPI_API void sapi_free_header(sapi_header_struct *sapi_header); SAPI_API void sapi_handle_post(void *arg); +SAPI_API void sapi_read_post_data(void); SAPI_API size_t sapi_read_post_block(char *buffer, size_t buflen); SAPI_API int sapi_register_post_entries(const sapi_post_entry *post_entry); SAPI_API int sapi_register_post_entry(const sapi_post_entry *post_entry); diff --git a/main/php_variables.c b/main/php_variables.c index 22d8ff73fe3a..db8094b0952e 100644 --- a/main/php_variables.c +++ b/main/php_variables.c @@ -25,6 +25,7 @@ #include "php_content_types.h" #include "SAPI.h" #include "zend_globals.h" +#include "zend_exceptions.h" /* for systems that need to override reading of environment variables */ void _php_import_environment_variables(zval *array_ptr); @@ -378,7 +379,7 @@ static bool add_post_var(zval *arr, post_var_data_t *var, bool eof) static inline int add_post_vars(zval *arr, post_var_data_t *vars, bool eof) { - uint64_t max_vars = PG(max_input_vars); + uint64_t max_vars = REQUEST_PARSE_BODY_OPTION_GET(max_input_vars, PG(max_input_vars)); vars->ptr = ZSTR_VAL(vars->str.s); vars->end = ZSTR_VAL(vars->str.s) + ZSTR_LEN(vars->str.s); @@ -538,8 +539,9 @@ SAPI_API SAPI_TREAT_DATA_FUNC(php_default_treat_data) } } - if (++count > PG(max_input_vars)) { - php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", PG(max_input_vars)); + zend_long max_input_vars = REQUEST_PARSE_BODY_OPTION_GET(max_input_vars, PG(max_input_vars)); + if (++count > max_input_vars) { + php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", max_input_vars); break; } diff --git a/main/rfc1867.c b/main/rfc1867.c index f16af170b6aa..3c069ed561fc 100644 --- a/main/rfc1867.c +++ b/main/rfc1867.c @@ -29,6 +29,7 @@ #include "php_variables.h" #include "rfc1867.h" #include "zend_smart_string.h" +#include "zend_exceptions.h" #ifndef DEBUG_FILE_UPLOAD # define DEBUG_FILE_UPLOAD 0 @@ -226,7 +227,6 @@ static int fill_buffer(multipart_buffer *self) while (bytes_to_read > 0) { char *buf = self->buffer + self->bytes_in_buffer; - actual_read = (int)sapi_module.read_post(buf, bytes_to_read); /* update the buffer length */ @@ -646,7 +646,7 @@ static char *multipart_buffer_read_body(multipart_buffer *self, size_t *len) * */ -SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ +SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) { char *boundary, *s = NULL, *boundary_end = NULL, *start_arr = NULL, *array_index = NULL; char *lbuf = NULL, *abuf = NULL; @@ -658,18 +658,30 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ HashTable *uploaded_files = NULL; multipart_buffer *mbuff; zval *array_ptr = (zval *) arg; + bool throw_exceptions = SG(request_parse_body_context).throw_exceptions; int fd = -1; zend_llist header; void *event_extra_data = NULL; unsigned int llen = 0; - int upload_cnt = INI_INT("max_file_uploads"); - int body_parts_cnt = INI_INT("max_multipart_body_parts"); + zend_long upload_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_file_uploads, INI_INT("max_file_uploads")); + zend_long body_parts_cnt = REQUEST_PARSE_BODY_OPTION_GET(max_multipart_body_parts, INI_INT("max_multipart_body_parts")); + zend_long post_max_size = REQUEST_PARSE_BODY_OPTION_GET(post_max_size, SG(post_max_size)); + zend_long max_input_vars = REQUEST_PARSE_BODY_OPTION_GET(max_input_vars, PG(max_input_vars)); + zend_long upload_max_filesize = REQUEST_PARSE_BODY_OPTION_GET(upload_max_filesize, PG(upload_max_filesize)); const zend_encoding *internal_encoding = zend_multibyte_get_internal_encoding(); php_rfc1867_getword_t getword; php_rfc1867_getword_conf_t getword_conf; php_rfc1867_basename_t _basename; zend_long count = 0; +#define EMIT_WARNING_OR_ERROR(...) do { \ + if (throw_exceptions) { \ + zend_throw_exception_ex(NULL, 0, __VA_ARGS__); \ + } else { \ + sapi_module.sapi_error(E_WARNING, __VA_ARGS__); \ + } \ + } while (0) + if (php_rfc1867_encoding_translation() && internal_encoding) { getword = php_rfc1867_getword; getword_conf = php_rfc1867_getword_conf; @@ -680,13 +692,13 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ _basename = php_ap_basename; } - if (SG(post_max_size) > 0 && SG(request_info).content_length > SG(post_max_size)) { - sapi_module.sapi_error(E_WARNING, "POST Content-Length of " ZEND_LONG_FMT " bytes exceeds the limit of " ZEND_LONG_FMT " bytes", SG(request_info).content_length, SG(post_max_size)); + if (post_max_size > 0 && SG(request_info).content_length > post_max_size) { + EMIT_WARNING_OR_ERROR("POST Content-Length of " ZEND_LONG_FMT " bytes exceeds the limit of " ZEND_LONG_FMT " bytes", SG(request_info).content_length, post_max_size); return; } if (body_parts_cnt < 0) { - body_parts_cnt = PG(max_input_vars) + upload_cnt; + body_parts_cnt = max_input_vars + upload_cnt; } int body_parts_limit = body_parts_cnt; @@ -705,7 +717,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } if (!boundary || !(boundary = strchr(boundary, '='))) { - sapi_module.sapi_error(E_WARNING, "Missing boundary in multipart/form-data POST data"); + EMIT_WARNING_OR_ERROR("Missing boundary in multipart/form-data POST data"); return; } @@ -716,7 +728,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ boundary++; boundary_end = strchr(boundary, '"'); if (!boundary_end) { - sapi_module.sapi_error(E_WARNING, "Invalid boundary in multipart/form-data POST data"); + EMIT_WARNING_OR_ERROR("Invalid boundary in multipart/form-data POST data"); return; } } else { @@ -729,10 +741,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } /* Initialize the buffer */ - if (!(mbuff = multipart_buffer_new(boundary, boundary_len))) { - sapi_module.sapi_error(E_WARNING, "Unable to initialize the input buffer"); - return; - } + mbuff = multipart_buffer_new(boundary, boundary_len); /* Initialize $_FILES[] */ zend_hash_init(&PG(rfc1867_protected_variables), 8, NULL, NULL, 0); @@ -775,7 +784,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ int end = 0; if (--body_parts_cnt < 0) { - php_error_docref(NULL, E_WARNING, "Multipart body parts limit exceeded %d. To increase the limit change max_multipart_body_parts in php.ini.", body_parts_limit); + EMIT_WARNING_OR_ERROR("Multipart body parts limit exceeded %d. To increase the limit change max_multipart_body_parts in php.ini.", body_parts_limit); goto fileupload_done; } @@ -849,7 +858,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } } - if (++count <= PG(max_input_vars) && sapi_module.input_filter(PARSE_POST, param, &value, value_len, &new_val_len)) { + if (++count <= max_input_vars && sapi_module.input_filter(PARSE_POST, param, &value, value_len, &new_val_len)) { if (php_rfc1867_callback != NULL) { multipart_event_formdata event_formdata; size_t newlength = new_val_len; @@ -868,8 +877,8 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } safe_php_register_variable(param, value, new_val_len, array_ptr, 0); } else { - if (count == PG(max_input_vars) + 1) { - php_error_docref(NULL, E_WARNING, "Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", PG(max_input_vars)); + if (count == max_input_vars + 1) { + EMIT_WARNING_OR_ERROR("Input variables exceeded " ZEND_LONG_FMT ". To increase the limit change max_input_vars in php.ini.", max_input_vars); } if (php_rfc1867_callback != NULL) { @@ -900,13 +909,13 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ skip_upload = 1; if (upload_cnt == 0) { --upload_cnt; - sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded"); + EMIT_WARNING_OR_ERROR("Maximum number of allowable file uploads has been exceeded"); } } /* Return with an error if the posted data is garbled */ if (!param && !filename) { - sapi_module.sapi_error(E_WARNING, "File Upload Mime headers garbled"); + EMIT_WARNING_OR_ERROR("File Upload Mime headers garbled"); goto fileupload_done; } @@ -988,7 +997,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ fd = php_open_temporary_fd_ex(PG(upload_tmp_dir), "php", &temp_filename, PHP_TMP_FILE_OPEN_BASEDIR_CHECK_ON_FALLBACK); upload_cnt--; if (fd == -1) { - sapi_module.sapi_error(E_WARNING, "File upload error - unable to create a temporary file"); + EMIT_WARNING_OR_ERROR("File upload error - unable to create a temporary file"); cancel_upload = PHP_UPLOAD_ERROR_E; } } @@ -1010,9 +1019,9 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } } - if (PG(upload_max_filesize) > 0 && (zend_long)(total_bytes+blen) > PG(upload_max_filesize)) { + if (upload_max_filesize > 0 && (zend_long)(total_bytes+blen) > upload_max_filesize) { #if DEBUG_FILE_UPLOAD - sapi_module.sapi_error(E_NOTICE, "upload_max_filesize of " ZEND_LONG_FMT " bytes exceeded - file [%s=%s] not saved", PG(upload_max_filesize), param, filename); + sapi_module.sapi_error(E_NOTICE, "upload_max_filesize of " ZEND_LONG_FMT " bytes exceeded - file [%s=%s] not saved", upload_max_filesize, param, filename); #endif cancel_upload = PHP_UPLOAD_ERROR_A; } else if (max_file_size && ((zend_long)(total_bytes+blen) > max_file_size)) { @@ -1060,7 +1069,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ } #if DEBUG_FILE_UPLOAD if (filename[0] != '\0' && total_bytes == 0 && !cancel_upload) { - sapi_module.sapi_error(E_WARNING, "Uploaded file size 0 - file [%s=%s] not saved", param, filename); + EMIT_WARNING_OR_ERROR("Uploaded file size 0 - file [%s=%s] not saved", param, filename); cancel_upload = 5; } #endif @@ -1134,7 +1143,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ register_http_post_files_variable(lbuf, s, &PG(http_globals)[TRACK_VARS_FILES], 0); s = NULL; - /* Add full path of supplied file for folder uploads via + /* Add full path of supplied file for folder uploads via * */ /* Add $foo[full_path] */ @@ -1263,6 +1272,8 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ if (mbuff->boundary) efree(mbuff->boundary); if (mbuff->buffer) efree(mbuff->buffer); if (mbuff) efree(mbuff); + +#undef EMIT_WARNING_OR_ERROR } /* }}} */ diff --git a/run-tests.php b/run-tests.php index be1bb86f729a..5837a1a85000 100755 --- a/run-tests.php +++ b/run-tests.php @@ -2324,7 +2324,9 @@ function run_test(string $php, $file, array $env): string } $env['CONTENT_LENGTH'] = strlen($request); - $env['REQUEST_METHOD'] = 'POST'; + if (empty($env['REQUEST_METHOD'])) { + $env['REQUEST_METHOD'] = 'POST'; + } if (empty($request)) { $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 4382e8647c31..e84defb3da46 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -2222,6 +2222,8 @@ static void php_cli_server_request_shutdown(php_cli_server *server, php_cli_serv destroy_request_info(&SG(request_info)); SG(server_context) = NULL; SG(rfc1867_uploaded_files) = NULL; + SG(request_parse_body_context).throw_exceptions = false; + memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); } /* }}} */ @@ -2315,6 +2317,8 @@ static zend_result php_cli_server_dispatch(php_cli_server *server, php_cli_serve sapi_module.send_headers = send_header_func; SG(sapi_headers).send_default_content_type = 1; SG(rfc1867_uploaded_files) = NULL; + SG(request_parse_body_context).throw_exceptions = false; + memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); } if (FAILURE == php_cli_server_begin_send_static(server, client)) { php_cli_server_close_connection(server, client); diff --git a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt index efb41c9054e7..702441a0773d 100644 --- a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt +++ b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt @@ -46,7 +46,7 @@ $tester->close(); ?> --EXPECT-- -Warning: PHP Request Startup: Multipart body parts limit exceeded 10. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 +Warning: Multipart body parts limit exceeded 10. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 int(10) --CLEAN-- close(); ?> --EXPECT-- -Warning: PHP Request Startup: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0 +Warning: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0 -Warning: PHP Request Startup: Multipart body parts limit exceeded 25. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 +Warning: Multipart body parts limit exceeded 25. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 int(20) --CLEAN-- +--FILE-- + $post, + 'files' => $files, + 'file_content' => $file_content, + 'post_global' => $_POST, + 'files_global' => $_FILES, +], JSON_PRETTY_PRINT); +EOT; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(); +$tester->expectLogStartNotices(); +echo $tester + ->request(method: 'PUT', stdin: [ + 'parts' => [ + [ + "disposition" => "form-data", + "param" => "name", + "name" => "get_parameter", + "value" => "foo", + ], + [ + "disposition" => "form-data", + "param" => "filename", + "name" => "uploaded_file", + "value" => "bar", + ], + ], + ]) + ->getBody(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +--EXPECTF-- +{ + "post": { + "get_parameter": "foo" + }, + "files": [ + { + "name": "uploaded_file", + "full_path": "uploaded_file", + "type": "", + "tmp_name": "%s", + "error": 0, + "size": 3 + } + ], + "file_content": "bar", + "post_global": [ + "post_global" + ], + "files_global": [ + "files_global" + ] +} +--CLEAN-- + diff --git a/sapi/fpm/tests/request_parse_body_urlencoded.phpt b/sapi/fpm/tests/request_parse_body_urlencoded.phpt new file mode 100644 index 000000000000..c81d02415af8 --- /dev/null +++ b/sapi/fpm/tests/request_parse_body_urlencoded.phpt @@ -0,0 +1,75 @@ +--TEST-- +PUT x-www-form-urlencoded +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--FILE-- + $post, + 'files' => $files, + 'post_global' => $_POST, + 'files_global' => $_FILES, +], JSON_PRETTY_PRINT); +EOT; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(); +$tester->expectLogStartNotices(); +echo $tester + ->request( + method: 'PUT', + headers: ['CONTENT_TYPE' => 'application/x-www-form-urlencoded'], + stdin: 'foo=foo&bar[]=1&bar[]=2' + ) + ->getBody(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +--CLEAN-- + +--EXPECT-- +{ + "post": { + "foo": "foo", + "bar": [ + "1", + "2" + ] + }, + "files": [], + "post_global": [ + "post_global" + ], + "files_global": [ + "files_global" + ] +} diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc index 05a33737ebb9..a196320353b0 100644 --- a/sapi/fpm/tests/tester.inc +++ b/sapi/fpm/tests/tester.inc @@ -697,7 +697,8 @@ class Tester string $uri = null, string $scriptFilename = null, string $scriptName = null, - ?string $stdin = null + ?string $stdin = null, + ?string $method = null, ): array { if (is_null($scriptFilename)) { $scriptFilename = $this->makeSourceFile(); @@ -712,7 +713,7 @@ class Tester $params = array_merge( [ 'GATEWAY_INTERFACE' => 'FastCGI/1.0', - 'REQUEST_METHOD' => is_null($stdin) ? 'GET' : 'POST', + 'REQUEST_METHOD' => $method ?? (is_null($stdin) ? 'GET' : 'POST'), 'SCRIPT_FILENAME' => $scriptFilename === '' ? null : $scriptFilename, 'SCRIPT_NAME' => $scriptName, 'QUERY_STRING' => $query, @@ -836,6 +837,7 @@ class Tester bool $expectError = false, int $readLimit = -1, int $writeDelay = 0, + ?string $method = null, ): Response { if ($this->hasError()) { return $this->createResponse(expectInvalid: true); @@ -845,7 +847,7 @@ class Tester $stdin = $this->parseStdin($stdin, $headers); } - $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $scriptName, $stdin); + $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $scriptName, $stdin, $method); $this->trace('Request params', $params); try { From 72a9f18e792ddaa6c813b09b6a95c319bbf3a95e Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 19:58:35 +0100 Subject: [PATCH 2/9] Print throwable class in tests --- Zend/tests/request_parse_body/invalid_boundary.phpt | 4 ++-- Zend/tests/request_parse_body/multipart_garbled.phpt | 4 ++-- Zend/tests/request_parse_body/multipart_max_files.phpt | 4 ++-- .../request_parse_body/multipart_max_input_vars.phpt | 4 ++-- Zend/tests/request_parse_body/multipart_max_parts.phpt | 4 ++-- .../request_parse_body/multipart_missing_boundary.phpt | 4 ++-- .../multipart_options_invalid_key.phpt | 10 +++++----- .../multipart_options_invalid_value_type.phpt | 6 +++--- .../multipart_options_max_files.phpt | 4 ++-- .../multipart_options_max_input_vars.phpt | 4 ++-- .../multipart_options_max_parts.phpt | 4 ++-- .../multipart_options_post_max_size.phpt | 4 ++-- .../multipart_options_upload_max_filesize.phpt | 2 +- .../request_parse_body/unsupported_content_type.phpt | 6 +++--- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Zend/tests/request_parse_body/invalid_boundary.phpt b/Zend/tests/request_parse_body/invalid_boundary.phpt index 4d7dba3751e6..76af155f72ad 100644 --- a/Zend/tests/request_parse_body/invalid_boundary.phpt +++ b/Zend/tests/request_parse_body/invalid_boundary.phpt @@ -11,14 +11,14 @@ empty try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Invalid boundary in multipart/form-data POST data +Exception: Invalid boundary in multipart/form-data POST data array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_garbled.phpt b/Zend/tests/request_parse_body/multipart_garbled.phpt index ee308de51994..802855ef99d3 100644 --- a/Zend/tests/request_parse_body/multipart_garbled.phpt +++ b/Zend/tests/request_parse_body/multipart_garbled.phpt @@ -18,14 +18,14 @@ post field data try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -File Upload Mime headers garbled +Exception: File Upload Mime headers garbled array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_files.phpt b/Zend/tests/request_parse_body/multipart_max_files.phpt index cef11763d9bb..8f50fd2cfdb2 100644 --- a/Zend/tests/request_parse_body/multipart_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_max_files.phpt @@ -23,14 +23,14 @@ file data try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Maximum number of allowable file uploads has been exceeded +Exception: Maximum number of allowable file uploads has been exceeded array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt index ce45bd46d83c..4fa9a903e228 100644 --- a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt @@ -21,14 +21,14 @@ post field data try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +Exception: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_parts.phpt b/Zend/tests/request_parse_body/multipart_max_parts.phpt index aa8445085e7d..c814f8824de5 100644 --- a/Zend/tests/request_parse_body/multipart_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_max_parts.phpt @@ -22,14 +22,14 @@ file data try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +Exception: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt index 860cbb2d0e41..a34fd3efc745 100644 --- a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt +++ b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt @@ -13,14 +13,14 @@ $stream = fopen('php://memory','r+'); try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Missing boundary in multipart/form-data POST data +Exception: Missing boundary in multipart/form-data POST data array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt index 0ec7d04070d5..acad79c9e338 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_key.phpt @@ -6,16 +6,16 @@ request_parse_body() invalid key try { request_parse_body(options: ['foo' => 1]); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } try { - request_parse_body(options: ['moo' => 1]); + request_parse_body(options: [42 => 1]); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } ?> --EXPECT-- -Invalid key "foo" in $options argument -Invalid key "moo" in $options argument +ValueError: Invalid key "foo" in $options argument +ValueError: Invalid integer key in $options argument diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt index 7dba161d41bf..3eba2de7bf30 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt @@ -8,9 +8,9 @@ try { 'max_input_vars' => [], ]); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo get_class($) $e->getMessage(), "\n"; } ?> ---EXPECT-- -Invalid array value in $options argument +--EXPECTF-- +Parse error: syntax error, unexpected token ")", expecting variable or "{" or "$" in %s on line %d diff --git a/Zend/tests/request_parse_body/multipart_options_max_files.phpt b/Zend/tests/request_parse_body/multipart_options_max_files.phpt index c3bda1be8215..6273e8bf8bab 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_files.phpt @@ -25,14 +25,14 @@ try { 'max_file_uploads' => 1, ]); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Maximum number of allowable file uploads has been exceeded +Exception: Maximum number of allowable file uploads has been exceeded array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt index 25773de53ca1..a09ce5b0665c 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt @@ -23,14 +23,14 @@ try { 'max_input_vars' => 1, ]); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +Exception: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt index 4cbb4265af68..4071e26e057b 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt @@ -24,14 +24,14 @@ try { 'max_multipart_body_parts' => 1, ]); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +Exception: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt index 7734b4135b4e..78fb09eb75d8 100644 --- a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt +++ b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt @@ -23,14 +23,14 @@ try { 'post_max_size' => '302', ]); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> --EXPECT-- -POST Content-Length of 303 bytes exceeds the limit of 302 bytes +Exception: POST Content-Length of 303 bytes exceeds the limit of 302 bytes array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt index 0e6cdde4150f..31a058536d52 100644 --- a/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt +++ b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt @@ -19,7 +19,7 @@ try { 'upload_max_filesize' => '128', ]); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); diff --git a/Zend/tests/request_parse_body/unsupported_content_type.phpt b/Zend/tests/request_parse_body/unsupported_content_type.phpt index 289189be0ef1..3ae016884fe4 100644 --- a/Zend/tests/request_parse_body/unsupported_content_type.phpt +++ b/Zend/tests/request_parse_body/unsupported_content_type.phpt @@ -13,14 +13,14 @@ Content-Type: application/json try { [$_POST, $_FILES] = request_parse_body(); } catch (Exception $e) { - echo $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } var_dump($_POST, $_FILES); ?> ---EXPECTF-- -Content-Type "application/json" is not supported +--EXPECT-- +Exception: Content-Type "application/json" is not supported array(0) { } array(0) { From 09689f50ebcb89ac230f33f923e95d4c5373eaa3 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 20:02:13 +0100 Subject: [PATCH 3/9] Move function to http.c --- Zend/Optimizer/zend_func_infos.h | 2 +- ext/standard/basic_functions.stub.php | 14 +-- ext/standard/basic_functions_arginfo.h | 14 +-- ext/standard/html.c | 122 ------------------------ ext/standard/http.c | 124 +++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 137 deletions(-) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index c3b8589ac004..16971fc178ea 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -525,7 +525,6 @@ static const func_info_t func_infos[] = { F1("htmlspecialchars", MAY_BE_STRING), F1("htmlentities", MAY_BE_STRING), F1("get_html_translation_table", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), - F1("request_parse_body", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ARRAY), F1("bin2hex", MAY_BE_STRING), F1("hex2bin", MAY_BE_STRING|MAY_BE_FALSE), #if defined(HAVE_NL_LANGINFO) @@ -600,6 +599,7 @@ static const func_info_t func_infos[] = { F1("fsockopen", MAY_BE_RESOURCE|MAY_BE_FALSE), FN("pfsockopen", MAY_BE_RESOURCE|MAY_BE_FALSE), F1("http_build_query", MAY_BE_STRING), + F1("request_parse_body", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ARRAY), F1("image_type_to_mime_type", MAY_BE_STRING), F1("image_type_to_extension", MAY_BE_STRING|MAY_BE_FALSE), F1("getimagesize", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_FALSE), diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 231c1e039f1e..ed73d16d6d0b 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2289,13 +2289,6 @@ function htmlentities(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | */ function get_html_translation_table(int $table = HTML_SPECIALCHARS, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, string $encoding = "UTF-8"): array {} -/** - * @param array|null $options - * @return array - * @refcount 1 - */ -function request_parse_body(?array $options = null): array {} - /* }}} */ /* assert.c */ @@ -3034,6 +3027,13 @@ function pfsockopen(string $hostname, int $port = -1, &$error_code = null, &$err /** @refcount 1 */ function http_build_query(array|object $data, string $numeric_prefix = "", ?string $arg_separator = null, int $encoding_type = PHP_QUERY_RFC1738): string {} +/** + * @param array|null $options + * @return array + * @refcount 1 + */ +function request_parse_body(?array $options = null): array {} + /* image.c */ /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index df67600642a3..4efdcb54546b 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: d45833d18fb6cd9db147418fac9bc22ce07bd3a9 */ + * Stub hash: 0bd0ac5d23881670cac81cda3e274cbee1e9a8dc */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -792,10 +792,6 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_get_html_translation_table, 0, 0 ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 0, "\"UTF-8\"") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_request_parse_body, 0, 0, IS_ARRAY, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") -ZEND_END_ARG_INFO() - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_assert, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, assertion, IS_MIXED, 0) ZEND_ARG_OBJ_TYPE_MASK(0, description, Throwable, MAY_BE_STRING|MAY_BE_NULL, "null") @@ -1510,6 +1506,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_http_build_query, 0, 1, IS_STRIN ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding_type, IS_LONG, 0, "PHP_QUERY_RFC1738") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_request_parse_body, 0, 0, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_image_type_to_mime_type, 0, 1, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, image_type, IS_LONG, 0) ZEND_END_ARG_INFO() @@ -2538,7 +2538,6 @@ ZEND_FUNCTION(htmlspecialchars_decode); ZEND_FUNCTION(html_entity_decode); ZEND_FUNCTION(htmlentities); ZEND_FUNCTION(get_html_translation_table); -ZEND_FUNCTION(request_parse_body); ZEND_FUNCTION(assert); ZEND_FUNCTION(assert_options); ZEND_FUNCTION(bin2hex); @@ -2720,6 +2719,7 @@ ZEND_FUNCTION(vfprintf); ZEND_FUNCTION(fsockopen); ZEND_FUNCTION(pfsockopen); ZEND_FUNCTION(http_build_query); +ZEND_FUNCTION(request_parse_body); ZEND_FUNCTION(image_type_to_mime_type); ZEND_FUNCTION(image_type_to_extension); ZEND_FUNCTION(getimagesize); @@ -3172,7 +3172,6 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(html_entity_decode, arginfo_html_entity_decode) ZEND_FE(htmlentities, arginfo_htmlentities) ZEND_FE(get_html_translation_table, arginfo_get_html_translation_table) - ZEND_FE(request_parse_body, arginfo_request_parse_body) ZEND_FE(assert, arginfo_assert) ZEND_DEP_FE(assert_options, arginfo_assert_options) ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(bin2hex, arginfo_bin2hex) @@ -3360,6 +3359,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(fsockopen, arginfo_fsockopen) ZEND_FE(pfsockopen, arginfo_pfsockopen) ZEND_FE(http_build_query, arginfo_http_build_query) + ZEND_FE(request_parse_body, arginfo_request_parse_body) ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(image_type_to_mime_type, arginfo_image_type_to_mime_type) ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(image_type_to_extension, arginfo_image_type_to_extension) ZEND_FE(getimagesize, arginfo_getimagesize) diff --git a/ext/standard/html.c b/ext/standard/html.c index a4e1940d70b7..1186b62d4fa8 100644 --- a/ext/standard/html.c +++ b/ext/standard/html.c @@ -50,7 +50,6 @@ #include #include "html_tables.h" -#include "zend_exceptions.h" /* Macro for disabling flag of translation of non-basic entities where this isn't supported. * Not appropriate for html_entity_decode/htmlspecialchars_decode */ @@ -1561,124 +1560,3 @@ PHP_FUNCTION(get_html_translation_table) } /* }}} */ -static zend_result cache_request_parse_body_option(HashTable *options, zval *option, int cache_offset) -{ - if (option) { - zend_long result; - 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)); - zend_string_release(errstr); - } - } else if (Z_TYPE_P(option) == IS_LONG) { - result = Z_LVAL_P(option); - } else { - zend_value_error("Invalid %s value in $options argument", zend_get_type_by_const(Z_TYPE_P(option))); - return FAILURE; - } - SG(request_parse_body_context).options_cache[cache_offset].set = true; - SG(request_parse_body_context).options_cache[cache_offset].value = result; - } else { - SG(request_parse_body_context).options_cache[cache_offset].set = false; - } - - return SUCCESS; -} - -static zend_result cache_request_parse_body_options(HashTable *options) -{ - zend_string *key; - zval *value; - ZEND_HASH_FOREACH_STR_KEY_VAL(options, key, value) { - if (!key) { - zend_value_error("Invalid integer key in $options argument"); - return FAILURE; - } - if (ZSTR_LEN(key) == 0) { - zend_value_error("Invalid empty string key in $options argument"); - return FAILURE; - } - -#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) { \ - return FAILURE; \ - } \ - continue; \ - } - - switch (ZSTR_VAL(key)[0]) { - case 'm': - case 'M': - CHECK_OPTION(max_file_uploads); - CHECK_OPTION(max_input_vars); - CHECK_OPTION(max_multipart_body_parts); - break; - case 'p': - case 'P': - CHECK_OPTION(post_max_size); - break; - case 'u': - case 'U': - CHECK_OPTION(upload_max_filesize); - break; - } - - zend_value_error("Invalid key \"%s\" in $options argument", ZSTR_VAL(key)); - return FAILURE; - } ZEND_HASH_FOREACH_END(); - -#undef CACHE_OPTION - - return SUCCESS; -} - -PHP_FUNCTION(request_parse_body) -{ - HashTable *options = NULL; - - ZEND_PARSE_PARAMETERS_START(0, 1) - Z_PARAM_OPTIONAL - Z_PARAM_ARRAY_HT_OR_NULL(options) - ZEND_PARSE_PARAMETERS_END(); - - SG(request_parse_body_context).throw_exceptions = true; - if (options) { - if (cache_request_parse_body_options(options) == FAILURE) { - goto exit; - } - } - - if (!SG(request_info).content_type) { - zend_throw_error(NULL, "Request does not provide a content type"); - goto exit; - } - - sapi_read_post_data(); - if (!SG(request_info).post_entry) { - zend_throw_exception_ex(NULL, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); - goto exit; - } - - zval post, files, old_post, old_files; - zval *global_post = &PG(http_globals)[TRACK_VARS_POST]; - zval *global_files = &PG(http_globals)[TRACK_VARS_FILES]; - - ZVAL_COPY_VALUE(&old_post, global_post); - ZVAL_COPY_VALUE(&old_files, global_files); - array_init(global_post); - array_init(global_files); - sapi_handle_post(global_post); - ZVAL_COPY_VALUE(&post, global_post); - ZVAL_COPY_VALUE(&files, global_files); - ZVAL_COPY_VALUE(global_post, &old_post); - ZVAL_COPY_VALUE(global_files, &old_files); - - RETVAL_ARR(zend_new_pair(&post, &files)); - -exit: - SG(request_parse_body_context).throw_exceptions = false; - memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); -} diff --git a/ext/standard/http.c b/ext/standard/http.c index 1145329a794f..906f8a6073c9 100644 --- a/ext/standard/http.c +++ b/ext/standard/http.c @@ -17,6 +17,8 @@ #include "php_http.h" #include "php_ini.h" #include "url.h" +#include "SAPI.h" +#include "zend_exceptions.h" static void php_url_encode_scalar(zval *scalar, smart_str *form_str, int encoding_type, zend_ulong index_int, @@ -235,3 +237,125 @@ PHP_FUNCTION(http_build_query) RETURN_STR(smart_str_extract(&formstr)); } /* }}} */ + +static zend_result cache_request_parse_body_option(HashTable *options, zval *option, int cache_offset) +{ + if (option) { + zend_long result; + 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)); + zend_string_release(errstr); + } + } else if (Z_TYPE_P(option) == IS_LONG) { + result = Z_LVAL_P(option); + } else { + zend_value_error("Invalid %s value in $options argument", zend_get_type_by_const(Z_TYPE_P(option))); + return FAILURE; + } + SG(request_parse_body_context).options_cache[cache_offset].set = true; + SG(request_parse_body_context).options_cache[cache_offset].value = result; + } else { + SG(request_parse_body_context).options_cache[cache_offset].set = false; + } + + return SUCCESS; +} + +static zend_result cache_request_parse_body_options(HashTable *options) +{ + zend_string *key; + zval *value; + ZEND_HASH_FOREACH_STR_KEY_VAL(options, key, value) { + if (!key) { + zend_value_error("Invalid integer key in $options argument"); + return FAILURE; + } + if (ZSTR_LEN(key) == 0) { + zend_value_error("Invalid empty string key in $options argument"); + return FAILURE; + } + +#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) { \ + return FAILURE; \ + } \ + continue; \ + } + + switch (ZSTR_VAL(key)[0]) { + case 'm': + case 'M': + CHECK_OPTION(max_file_uploads); + CHECK_OPTION(max_input_vars); + CHECK_OPTION(max_multipart_body_parts); + break; + case 'p': + case 'P': + CHECK_OPTION(post_max_size); + break; + case 'u': + case 'U': + CHECK_OPTION(upload_max_filesize); + break; + } + + zend_value_error("Invalid key \"%s\" in $options argument", ZSTR_VAL(key)); + return FAILURE; + } ZEND_HASH_FOREACH_END(); + +#undef CACHE_OPTION + + return SUCCESS; +} + +PHP_FUNCTION(request_parse_body) +{ + HashTable *options = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT_OR_NULL(options) + ZEND_PARSE_PARAMETERS_END(); + + SG(request_parse_body_context).throw_exceptions = true; + if (options) { + if (cache_request_parse_body_options(options) == FAILURE) { + goto exit; + } + } + + if (!SG(request_info).content_type) { + zend_throw_error(NULL, "Request does not provide a content type"); + goto exit; + } + + sapi_read_post_data(); + if (!SG(request_info).post_entry) { + zend_throw_exception_ex(NULL, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); + goto exit; + } + + zval post, files, old_post, old_files; + zval *global_post = &PG(http_globals)[TRACK_VARS_POST]; + zval *global_files = &PG(http_globals)[TRACK_VARS_FILES]; + + ZVAL_COPY_VALUE(&old_post, global_post); + ZVAL_COPY_VALUE(&old_files, global_files); + array_init(global_post); + array_init(global_files); + sapi_handle_post(global_post); + ZVAL_COPY_VALUE(&post, global_post); + ZVAL_COPY_VALUE(&files, global_files); + ZVAL_COPY_VALUE(global_post, &old_post); + ZVAL_COPY_VALUE(global_files, &old_files); + + RETVAL_ARR(zend_new_pair(&post, &files)); + +exit: + SG(request_parse_body_context).throw_exceptions = false; + memset(&SG(request_parse_body_context).options_cache, 0, sizeof(SG(request_parse_body_context).options_cache)); +} From 1ca28cd45b7f22a34a338c085ddebf9ccfec7130 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 20:22:40 +0100 Subject: [PATCH 4/9] Improve error messages --- .../{multipart_stream.phpt => multipart.phpt} | 2 +- .../multipart_options_invalid_quantity.phpt | 17 +++++++++++++++++ .../multipart_options_invalid_value_type.phpt | 6 +++--- .../{urlencoded_stream.phpt => urlencoded.phpt} | 2 +- ext/standard/http.c | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) rename Zend/tests/request_parse_body/{multipart_stream.phpt => multipart.phpt} (96%) create mode 100644 Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt rename Zend/tests/request_parse_body/{urlencoded_stream.phpt => urlencoded.phpt} (91%) diff --git a/Zend/tests/request_parse_body/multipart_stream.phpt b/Zend/tests/request_parse_body/multipart.phpt similarity index 96% rename from Zend/tests/request_parse_body/multipart_stream.phpt rename to Zend/tests/request_parse_body/multipart.phpt index 94bfd254e3b0..50f4a7c09d1b 100644 --- a/Zend/tests/request_parse_body/multipart_stream.phpt +++ b/Zend/tests/request_parse_body/multipart.phpt @@ -1,5 +1,5 @@ --TEST-- -request_parse_body() with multipart stream +request_parse_body() with multipart --ENV-- REQUEST_METHOD=PUT --POST_RAW-- diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt new file mode 100644 index 000000000000..4d88b5fdcf66 --- /dev/null +++ b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt @@ -0,0 +1,17 @@ +--TEST-- +request_parse_body() invalid quantity +--FILE-- + '1GB', + ]); +} catch (Error $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 +Error: Request does not provide a content type diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt index 3eba2de7bf30..76b4e2a30fca 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt @@ -8,9 +8,9 @@ try { 'max_input_vars' => [], ]); } catch (Error $e) { - echo get_class($) $e->getMessage(), "\n"; + echo get_class($e) . ': ' . $e->getMessage(), "\n"; } ?> ---EXPECTF-- -Parse error: syntax error, unexpected token ")", expecting variable or "{" or "$" in %s on line %d +--EXPECT-- +ValueError: Invalid array value in $options argument diff --git a/Zend/tests/request_parse_body/urlencoded_stream.phpt b/Zend/tests/request_parse_body/urlencoded.phpt similarity index 91% rename from Zend/tests/request_parse_body/urlencoded_stream.phpt rename to Zend/tests/request_parse_body/urlencoded.phpt index 384e48de9a65..b2400c8ae3f0 100644 --- a/Zend/tests/request_parse_body/urlencoded_stream.phpt +++ b/Zend/tests/request_parse_body/urlencoded.phpt @@ -1,5 +1,5 @@ --TEST-- -request_parse_body() with urlencoded stream +request_parse_body() with urlencoded --ENV-- REQUEST_METHOD=PUT --POST_RAW-- diff --git a/ext/standard/http.c b/ext/standard/http.c index 906f8a6073c9..16dac0e46b6b 100644 --- a/ext/standard/http.c +++ b/ext/standard/http.c @@ -252,7 +252,7 @@ static zend_result cache_request_parse_body_option(HashTable *options, zval *opt } else if (Z_TYPE_P(option) == IS_LONG) { result = Z_LVAL_P(option); } else { - zend_value_error("Invalid %s value in $options argument", zend_get_type_by_const(Z_TYPE_P(option))); + zend_value_error("Invalid %s value in $options argument", zend_zval_value_name(option)); return FAILURE; } SG(request_parse_body_context).options_cache[cache_offset].set = true; From a562903bee9ac44fbe37e8ab7b184e0a6c37fdaa Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 20:45:50 +0100 Subject: [PATCH 5/9] Add RequestParseBodyException --- .../request_parse_body/invalid_boundary.phpt | 2 +- .../request_parse_body/multipart_garbled.phpt | 2 +- .../request_parse_body/multipart_max_files.phpt | 2 +- .../multipart_max_input_vars.phpt | 2 +- .../request_parse_body/multipart_max_parts.phpt | 2 +- .../multipart_missing_boundary.phpt | 2 +- .../multipart_options_max_files.phpt | 2 +- .../multipart_options_max_input_vars.phpt | 2 +- .../multipart_options_max_parts.phpt | 2 +- .../multipart_options_post_max_size.phpt | 2 +- Zend/zend_exceptions.c | 4 ++++ Zend/zend_exceptions.h | 1 + Zend/zend_exceptions.stub.php | 4 ++++ Zend/zend_exceptions_arginfo.h | 17 ++++++++++++++++- main/rfc1867.c | 2 +- 15 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Zend/tests/request_parse_body/invalid_boundary.phpt b/Zend/tests/request_parse_body/invalid_boundary.phpt index 76af155f72ad..bef892418471 100644 --- a/Zend/tests/request_parse_body/invalid_boundary.phpt +++ b/Zend/tests/request_parse_body/invalid_boundary.phpt @@ -18,7 +18,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Invalid boundary in multipart/form-data POST data +RequestParseBodyException: Invalid boundary in multipart/form-data POST data array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_garbled.phpt b/Zend/tests/request_parse_body/multipart_garbled.phpt index 802855ef99d3..f0b2bea940c8 100644 --- a/Zend/tests/request_parse_body/multipart_garbled.phpt +++ b/Zend/tests/request_parse_body/multipart_garbled.phpt @@ -25,7 +25,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: File Upload Mime headers garbled +RequestParseBodyException: File Upload Mime headers garbled array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_files.phpt b/Zend/tests/request_parse_body/multipart_max_files.phpt index 8f50fd2cfdb2..8b635dacf67e 100644 --- a/Zend/tests/request_parse_body/multipart_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_max_files.phpt @@ -30,7 +30,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Maximum number of allowable file uploads has been exceeded +RequestParseBodyException: Maximum number of allowable file uploads has been exceeded array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt index 4fa9a903e228..e4bc32115cde 100644 --- a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt @@ -28,7 +28,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +RequestParseBodyException: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_max_parts.phpt b/Zend/tests/request_parse_body/multipart_max_parts.phpt index c814f8824de5..17e590495753 100644 --- a/Zend/tests/request_parse_body/multipart_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_max_parts.phpt @@ -29,7 +29,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +RequestParseBodyException: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt index a34fd3efc745..e9ed0361c58a 100644 --- a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt +++ b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt @@ -20,7 +20,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Missing boundary in multipart/form-data POST data +RequestParseBodyException: Missing boundary in multipart/form-data POST data array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_max_files.phpt b/Zend/tests/request_parse_body/multipart_options_max_files.phpt index 6273e8bf8bab..6500bd64090e 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_files.phpt @@ -32,7 +32,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Maximum number of allowable file uploads has been exceeded +RequestParseBodyException: Maximum number of allowable file uploads has been exceeded array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt index a09ce5b0665c..1045dd87ffba 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt @@ -30,7 +30,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. +RequestParseBodyException: Input variables exceeded 1. To increase the limit change max_input_vars in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt index 4071e26e057b..73898bd6fa06 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt @@ -31,7 +31,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. +RequestParseBodyException: Multipart body parts limit exceeded 1. To increase the limit change max_multipart_body_parts in php.ini. array(0) { } array(0) { diff --git a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt index 78fb09eb75d8..8916910d2250 100644 --- a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt +++ b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt @@ -30,7 +30,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: POST Content-Length of 303 bytes exceeds the limit of 302 bytes +RequestParseBodyException: POST Content-Length of 303 bytes exceeds the limit of 302 bytes array(0) { } array(0) { diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index da2ed2c12d4d..2c6a432096d7 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -42,6 +42,7 @@ ZEND_API zend_class_entry *zend_ce_value_error; ZEND_API zend_class_entry *zend_ce_arithmetic_error; ZEND_API zend_class_entry *zend_ce_division_by_zero_error; ZEND_API zend_class_entry *zend_ce_unhandled_match_error; +ZEND_API zend_class_entry *zend_ce_request_parse_body_exception; /* Internal pseudo-exception that is not exposed to userland. Throwing this exception *does not* execute finally blocks. */ static zend_class_entry zend_ce_unwind_exit; @@ -788,6 +789,9 @@ void zend_register_default_exception(void) /* {{{ */ zend_ce_unhandled_match_error = register_class_UnhandledMatchError(zend_ce_error); zend_init_exception_class_entry(zend_ce_unhandled_match_error); + zend_ce_request_parse_body_exception = register_class_RequestParseBodyException(zend_ce_exception); + zend_init_exception_class_entry(zend_ce_request_parse_body_exception); + INIT_CLASS_ENTRY(zend_ce_unwind_exit, "UnwindExit", NULL); INIT_CLASS_ENTRY(zend_ce_graceful_exit, "GracefulExit", NULL); diff --git a/Zend/zend_exceptions.h b/Zend/zend_exceptions.h index f61b5ecb304e..242b71a8be05 100644 --- a/Zend/zend_exceptions.h +++ b/Zend/zend_exceptions.h @@ -36,6 +36,7 @@ extern ZEND_API zend_class_entry *zend_ce_value_error; extern ZEND_API zend_class_entry *zend_ce_arithmetic_error; extern ZEND_API zend_class_entry *zend_ce_division_by_zero_error; extern ZEND_API zend_class_entry *zend_ce_unhandled_match_error; +extern ZEND_API zend_class_entry *zend_ce_request_parse_body_exception; ZEND_API void zend_exception_set_previous(zend_object *exception, zend_object *add_previous); ZEND_API void zend_exception_save(void); diff --git a/Zend/zend_exceptions.stub.php b/Zend/zend_exceptions.stub.php index 84109b021a74..86f2838ee912 100644 --- a/Zend/zend_exceptions.stub.php +++ b/Zend/zend_exceptions.stub.php @@ -170,3 +170,7 @@ class DivisionByZeroError extends ArithmeticError class UnhandledMatchError extends Error { } + +class RequestParseBodyException extends Exception +{ +} diff --git a/Zend/zend_exceptions_arginfo.h b/Zend/zend_exceptions_arginfo.h index c0e274d64ef7..0971b5ca3e95 100644 --- a/Zend/zend_exceptions_arginfo.h +++ b/Zend/zend_exceptions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 4cf2c620393f468968a219b5bd12a2b5f6b03ecc */ + * Stub hash: ba1562ca8fe2fe48c40bc52d10545aa989afd86c */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Throwable_getMessage, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -187,6 +187,11 @@ static const zend_function_entry class_UnhandledMatchError_methods[] = { ZEND_FE_END }; + +static const zend_function_entry class_RequestParseBodyException_methods[] = { + ZEND_FE_END +}; + static zend_class_entry *register_class_Throwable(zend_class_entry *class_entry_Stringable) { zend_class_entry ce, *class_entry; @@ -401,3 +406,13 @@ static zend_class_entry *register_class_UnhandledMatchError(zend_class_entry *cl return class_entry; } + +static zend_class_entry *register_class_RequestParseBodyException(zend_class_entry *class_entry_Exception) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "RequestParseBodyException", class_RequestParseBodyException_methods); + class_entry = zend_register_internal_class_ex(&ce, class_entry_Exception); + + return class_entry; +} diff --git a/main/rfc1867.c b/main/rfc1867.c index 3c069ed561fc..c0170046370d 100644 --- a/main/rfc1867.c +++ b/main/rfc1867.c @@ -676,7 +676,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) #define EMIT_WARNING_OR_ERROR(...) do { \ if (throw_exceptions) { \ - zend_throw_exception_ex(NULL, 0, __VA_ARGS__); \ + zend_throw_exception_ex(zend_ce_request_parse_body_exception, 0, __VA_ARGS__); \ } else { \ sapi_module.sapi_error(E_WARNING, __VA_ARGS__); \ } \ From 856b50996a06887b0fdfff2944d323db68bc296a Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 20:51:23 +0100 Subject: [PATCH 6/9] More exception stuff --- Zend/tests/request_parse_body/invalid_boundary.phpt | 2 +- Zend/tests/request_parse_body/multipart_garbled.phpt | 2 +- Zend/tests/request_parse_body/multipart_max_files.phpt | 2 +- Zend/tests/request_parse_body/multipart_max_input_vars.phpt | 2 +- Zend/tests/request_parse_body/multipart_max_parts.phpt | 2 +- .../request_parse_body/multipart_missing_boundary.phpt | 2 +- .../multipart_options_invalid_quantity.phpt | 6 +++--- .../multipart_options_invalid_value_type.phpt | 2 +- .../request_parse_body/multipart_options_max_files.phpt | 2 +- .../multipart_options_max_input_vars.phpt | 2 +- .../request_parse_body/multipart_options_max_parts.phpt | 2 +- .../request_parse_body/multipart_options_post_max_size.phpt | 2 +- .../multipart_options_upload_max_filesize.phpt | 2 +- Zend/tests/request_parse_body/unsupported_content_type.phpt | 4 ++-- ext/standard/http.c | 5 +++-- 15 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Zend/tests/request_parse_body/invalid_boundary.phpt b/Zend/tests/request_parse_body/invalid_boundary.phpt index bef892418471..980b44e94899 100644 --- a/Zend/tests/request_parse_body/invalid_boundary.phpt +++ b/Zend/tests/request_parse_body/invalid_boundary.phpt @@ -10,7 +10,7 @@ empty try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_garbled.phpt b/Zend/tests/request_parse_body/multipart_garbled.phpt index f0b2bea940c8..521de0805bf4 100644 --- a/Zend/tests/request_parse_body/multipart_garbled.phpt +++ b/Zend/tests/request_parse_body/multipart_garbled.phpt @@ -17,7 +17,7 @@ post field data try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_max_files.phpt b/Zend/tests/request_parse_body/multipart_max_files.phpt index 8b635dacf67e..9c4fe6c3cd7b 100644 --- a/Zend/tests/request_parse_body/multipart_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_max_files.phpt @@ -22,7 +22,7 @@ file data try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt index e4bc32115cde..384344e6c176 100644 --- a/Zend/tests/request_parse_body/multipart_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_max_input_vars.phpt @@ -20,7 +20,7 @@ post field data try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_max_parts.phpt b/Zend/tests/request_parse_body/multipart_max_parts.phpt index 17e590495753..776741003037 100644 --- a/Zend/tests/request_parse_body/multipart_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_max_parts.phpt @@ -21,7 +21,7 @@ file data try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt index e9ed0361c58a..9656071c765b 100644 --- a/Zend/tests/request_parse_body/multipart_missing_boundary.phpt +++ b/Zend/tests/request_parse_body/multipart_missing_boundary.phpt @@ -12,7 +12,7 @@ $stream = fopen('php://memory','r+'); try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt index 4d88b5fdcf66..48be811e7a39 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt @@ -7,11 +7,11 @@ try { request_parse_body(options: [ 'upload_max_filesize' => '1GB', ]); -} catch (Error $e) { - echo get_class($e) . ': ' . $e->getMessage(), "\n"; +} 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 -Error: Request does not provide a content type +InvalidArgumentException: Request does not provide a content type diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt index 76b4e2a30fca..b61416cdf13d 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_value_type.phpt @@ -8,7 +8,7 @@ try { 'max_input_vars' => [], ]); } catch (Error $e) { - echo get_class($e) . ': ' . $e->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; } ?> diff --git a/Zend/tests/request_parse_body/multipart_options_max_files.phpt b/Zend/tests/request_parse_body/multipart_options_max_files.phpt index 6500bd64090e..ad0c2a906237 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_files.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_files.phpt @@ -24,7 +24,7 @@ try { [$_POST, $_FILES] = request_parse_body([ 'max_file_uploads' => 1, ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt index 1045dd87ffba..990102abd853 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_input_vars.phpt @@ -22,7 +22,7 @@ try { [$_POST, $_FILES] = request_parse_body([ 'max_input_vars' => 1, ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt index 73898bd6fa06..e9d8d51b2428 100644 --- a/Zend/tests/request_parse_body/multipart_options_max_parts.phpt +++ b/Zend/tests/request_parse_body/multipart_options_max_parts.phpt @@ -23,7 +23,7 @@ try { [$_POST, $_FILES] = request_parse_body([ 'max_multipart_body_parts' => 1, ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt index 8916910d2250..7a66faa7dba4 100644 --- a/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt +++ b/Zend/tests/request_parse_body/multipart_options_post_max_size.phpt @@ -22,7 +22,7 @@ try { [$_POST, $_FILES] = request_parse_body([ 'post_max_size' => '302', ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt index 31a058536d52..981a1d6799d1 100644 --- a/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt +++ b/Zend/tests/request_parse_body/multipart_options_upload_max_filesize.phpt @@ -18,7 +18,7 @@ try { [$_POST, $_FILES] = request_parse_body([ 'upload_max_filesize' => '128', ]); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } diff --git a/Zend/tests/request_parse_body/unsupported_content_type.phpt b/Zend/tests/request_parse_body/unsupported_content_type.phpt index 3ae016884fe4..bd4e3f4ff2af 100644 --- a/Zend/tests/request_parse_body/unsupported_content_type.phpt +++ b/Zend/tests/request_parse_body/unsupported_content_type.phpt @@ -12,7 +12,7 @@ Content-Type: application/json try { [$_POST, $_FILES] = request_parse_body(); -} catch (Exception $e) { +} catch (Throwable $e) { echo get_class($e), ': ', $e->getMessage(), "\n"; } @@ -20,7 +20,7 @@ var_dump($_POST, $_FILES); ?> --EXPECT-- -Exception: Content-Type "application/json" is not supported +InvalidArgumentException: Content-Type "application/json" is not supported array(0) { } array(0) { diff --git a/ext/standard/http.c b/ext/standard/http.c index 16dac0e46b6b..53c35fd9f6ac 100644 --- a/ext/standard/http.c +++ b/ext/standard/http.c @@ -19,6 +19,7 @@ #include "url.h" #include "SAPI.h" #include "zend_exceptions.h" +#include "ext/spl/spl_exceptions.h" static void php_url_encode_scalar(zval *scalar, smart_str *form_str, int encoding_type, zend_ulong index_int, @@ -329,13 +330,13 @@ PHP_FUNCTION(request_parse_body) } if (!SG(request_info).content_type) { - zend_throw_error(NULL, "Request does not provide a content type"); + zend_throw_error(spl_ce_InvalidArgumentException, "Request does not provide a content type"); goto exit; } sapi_read_post_data(); if (!SG(request_info).post_entry) { - zend_throw_exception_ex(NULL, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); + zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); goto exit; } From dd14fd004db9dbe28a89109483d76e3a63594589 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 21:01:45 +0100 Subject: [PATCH 7/9] Retain RFC1867 error message format --- main/rfc1867.c | 2 +- sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt | 2 +- .../fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-default.phpt | 4 ++-- sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-file-uploads.phpt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main/rfc1867.c b/main/rfc1867.c index c0170046370d..99ac150a6ffc 100644 --- a/main/rfc1867.c +++ b/main/rfc1867.c @@ -678,7 +678,7 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) if (throw_exceptions) { \ zend_throw_exception_ex(zend_ce_request_parse_body_exception, 0, __VA_ARGS__); \ } else { \ - sapi_module.sapi_error(E_WARNING, __VA_ARGS__); \ + php_error_docref(NULL, E_WARNING, __VA_ARGS__); \ } \ } while (0) diff --git a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt index 702441a0773d..efb41c9054e7 100644 --- a/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt +++ b/sapi/fpm/tests/ghsa-54hq-v5wp-fqgv-max-body-parts-custom.phpt @@ -46,7 +46,7 @@ $tester->close(); ?> --EXPECT-- -Warning: Multipart body parts limit exceeded 10. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 +Warning: PHP Request Startup: Multipart body parts limit exceeded 10. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 int(10) --CLEAN-- close(); ?> --EXPECT-- -Warning: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0 +Warning: PHP Request Startup: Input variables exceeded 20. To increase the limit change max_input_vars in php.ini. in Unknown on line 0 -Warning: Multipart body parts limit exceeded 25. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 +Warning: PHP Request Startup: Multipart body parts limit exceeded 25. To increase the limit change max_multipart_body_parts in php.ini. in Unknown on line 0 int(20) --CLEAN-- close(); ?> --EXPECT-- -Warning: Maximum number of allowable file uploads has been exceeded in Unknown on line 0 +Warning: PHP Request Startup: Maximum number of allowable file uploads has been exceeded in Unknown on line 0 int(5) --CLEAN-- Date: Wed, 7 Feb 2024 21:06:47 +0100 Subject: [PATCH 8/9] Fix more tests --- tests/basic/rfc1867_garbled_mime_headers.phpt | 2 +- tests/basic/rfc1867_invalid_boundary.phpt | 2 +- tests/basic/rfc1867_missing_boundary.phpt | 2 +- tests/basic/rfc1867_post_max_size.phpt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/basic/rfc1867_garbled_mime_headers.phpt b/tests/basic/rfc1867_garbled_mime_headers.phpt index a6c0425335f1..9a8c7e7ec5b0 100644 --- a/tests/basic/rfc1867_garbled_mime_headers.phpt +++ b/tests/basic/rfc1867_garbled_mime_headers.phpt @@ -16,7 +16,7 @@ var_dump($_FILES); var_dump($_POST); ?> --EXPECTF-- -Warning: File Upload Mime headers garbled in %s +Warning: PHP Request Startup: File Upload Mime headers garbled in %s array(0) { } array(0) { diff --git a/tests/basic/rfc1867_invalid_boundary.phpt b/tests/basic/rfc1867_invalid_boundary.phpt index 3b63b2722206..d8557840bd01 100644 --- a/tests/basic/rfc1867_invalid_boundary.phpt +++ b/tests/basic/rfc1867_invalid_boundary.phpt @@ -15,7 +15,7 @@ var_dump($_FILES); var_dump($_POST); ?> --EXPECTF-- -Warning: Invalid boundary in multipart/form-data POST data in %s +Warning: PHP Request Startup: Invalid boundary in multipart/form-data POST data in %s array(0) { } array(0) { diff --git a/tests/basic/rfc1867_missing_boundary.phpt b/tests/basic/rfc1867_missing_boundary.phpt index aadd1a8aed03..32dd8b149e22 100644 --- a/tests/basic/rfc1867_missing_boundary.phpt +++ b/tests/basic/rfc1867_missing_boundary.phpt @@ -15,7 +15,7 @@ var_dump($_FILES); var_dump($_POST); ?> --EXPECTF-- -Warning: Missing boundary in multipart/form-data POST data in %s +Warning: PHP Request Startup: Missing boundary in multipart/form-data POST data in %s array(0) { } array(0) { diff --git a/tests/basic/rfc1867_post_max_size.phpt b/tests/basic/rfc1867_post_max_size.phpt index eba547c7d9a0..817695348593 100644 --- a/tests/basic/rfc1867_post_max_size.phpt +++ b/tests/basic/rfc1867_post_max_size.phpt @@ -15,7 +15,7 @@ var_dump($_FILES); var_dump($_POST); ?> --EXPECTF-- -Warning: POST Content-Length of %d bytes exceeds the limit of 1 bytes in %s +Warning: PHP Request Startup: POST Content-Length of 168 bytes exceeds the limit of 1 bytes in %s array(0) { } array(0) { From e65933494df8d3c9495852c6332c171c27f1f38b Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 7 Feb 2024 21:25:38 +0100 Subject: [PATCH 9/9] Review fixes --- .../multipart_options_invalid_quantity.phpt | 2 +- ext/standard/html.c | 1 - ext/standard/http.c | 4 ++-- main/rfc1867.c | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt index 48be811e7a39..b1efa0dbc91a 100644 --- a/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt +++ b/Zend/tests/request_parse_body/multipart_options_invalid_quantity.phpt @@ -14,4 +14,4 @@ try { ?> --EXPECTF-- Warning: Invalid quantity "1GB": unknown multiplier "B", interpreting as "1" for backwards compatibility in %s on line %d -InvalidArgumentException: Request does not provide a content type +RequestParseBodyException: Request does not provide a content type diff --git a/ext/standard/html.c b/ext/standard/html.c index 1186b62d4fa8..731ee340185b 100644 --- a/ext/standard/html.c +++ b/ext/standard/html.c @@ -1559,4 +1559,3 @@ PHP_FUNCTION(get_html_translation_table) } } /* }}} */ - diff --git a/ext/standard/http.c b/ext/standard/http.c index 53c35fd9f6ac..da1fdeb39286 100644 --- a/ext/standard/http.c +++ b/ext/standard/http.c @@ -330,13 +330,13 @@ PHP_FUNCTION(request_parse_body) } if (!SG(request_info).content_type) { - zend_throw_error(spl_ce_InvalidArgumentException, "Request does not provide a content type"); + zend_throw_error(zend_ce_request_parse_body_exception, "Request does not provide a content type"); goto exit; } sapi_read_post_data(); if (!SG(request_info).post_entry) { - zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Content-Type \"%s\" is not supported", SG(request_info).content_type); + zend_throw_error(spl_ce_InvalidArgumentException, "Content-Type \"%s\" is not supported", SG(request_info).content_type); goto exit; } diff --git a/main/rfc1867.c b/main/rfc1867.c index 99ac150a6ffc..4292f5065bc0 100644 --- a/main/rfc1867.c +++ b/main/rfc1867.c @@ -227,6 +227,7 @@ static int fill_buffer(multipart_buffer *self) while (bytes_to_read > 0) { char *buf = self->buffer + self->bytes_in_buffer; + actual_read = (int)sapi_module.read_post(buf, bytes_to_read); /* update the buffer length */