From 22aeec74e8473dba837a4d5ae09739e79b9f78a2 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 6 Apr 2026 18:00:24 -0400 Subject: [PATCH 1/2] Fix GH-18105: Wrong file & line on exception trace with trait default parameter When a trait method has a default parameter using `new` and the constructor throws, the exception's file/line pointed to the class using the trait rather than where the throw actually occurred. `EG(filename_override)`, set to the scope's filename during AST evaluation, leaked into the constructor's VM re-entry. Clear the override before constructor calls in `zend_ast_evaluate_inner`'s ZEND_AST_NEW handler so normal stack frame resolution applies. Closes GH-18105 --- Zend/tests/gh18105.phpt | 37 +++++++++++++++++++++++++++++++++++++ Zend/zend_ast.c | 12 ++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 Zend/tests/gh18105.phpt diff --git a/Zend/tests/gh18105.phpt b/Zend/tests/gh18105.phpt new file mode 100644 index 0000000000000..30bebb2823d9d --- /dev/null +++ b/Zend/tests/gh18105.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-18105 (Wrong line & file on error trace with trait default parameter) +--FILE-- +execute(); +} catch (Exception $e) { + echo basename($e->getFile()), "\n"; + echo $e->getLine(), "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +gh18105_defs.inc +4 diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index cc4a2a8226fe6..7916e7ed5c8d7 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1099,8 +1099,14 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( zend_function *ctor = Z_OBJ_HT_P(result)->get_constructor(Z_OBJ_P(result)); if (ctor) { + zend_string *prev_filename = EG(filename_override); + zend_long prev_lineno = EG(lineno_override); + EG(filename_override) = NULL; + EG(lineno_override) = -1; zend_call_known_function( ctor, Z_OBJ_P(result), Z_OBJCE_P(result), NULL, 0, NULL, args); + EG(filename_override) = prev_filename; + EG(lineno_override) = prev_lineno; } zend_array_destroy(args); @@ -1120,8 +1126,14 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( zend_function *ctor = Z_OBJ_HT_P(result)->get_constructor(Z_OBJ_P(result)); if (ctor) { + zend_string *prev_filename = EG(filename_override); + zend_long prev_lineno = EG(lineno_override); + EG(filename_override) = NULL; + EG(lineno_override) = -1; zend_call_known_instance_method( ctor, Z_OBJ_P(result), NULL, args_ast->children, args); + EG(filename_override) = prev_filename; + EG(lineno_override) = prev_lineno; } for (uint32_t i = 0; i < args_ast->children; i++) { From 76e541099ee8712de4b7e039f63851afbd531596 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 6 Apr 2026 19:08:29 -0400 Subject: [PATCH 2/2] Fix GH-17976: CRLF injection via from and user_agent in HTTP wrapper The from and user_agent INI settings and the user_agent stream context option were written into HTTP request headers without stripping CR/LF characters, allowing header injection. Truncate at the first \r or \n and emit E_WARNING. Closes GH-17976 --- ext/standard/http_fopen_wrapper.c | 34 ++++++++--------- ext/standard/tests/http/gh17976.phpt | 57 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 ext/standard/tests/http/gh17976.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 6d9f7331afb31..11f9b336f752a 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -353,6 +353,16 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w return NULL; } +static inline void smart_str_append_header_value(smart_str *dest, const char *src, const char *header_name) +{ + size_t len = strcspn(src, "\r\n"); + smart_str_appendl(dest, src, len); + if (src[len] != '\0') { + php_error_docref(NULL, E_WARNING, + "Header %s value contains newline characters and has been truncated", header_name); + } +} + static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context, int redirect_max, int flags, @@ -784,7 +794,7 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, /* if the user has configured who they are, send a From: line */ if (!(have_header & HTTP_HEADER_FROM) && FG(from_address)) { smart_str_appends(&req_buf, "From: "); - smart_str_appends(&req_buf, FG(from_address)); + smart_str_append_header_value(&req_buf, FG(from_address), "From"); smart_str_appends(&req_buf, "\r\n"); } @@ -818,24 +828,10 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, ua_str = FG(user_agent); } - if (((have_header & HTTP_HEADER_USER_AGENT) == 0) && ua_str) { -#define _UA_HEADER "User-Agent: %s\r\n" - char *ua; - size_t ua_len; - - ua_len = sizeof(_UA_HEADER) + strlen(ua_str); - - /* ensure the header is only sent if user_agent is not blank */ - if (ua_len > sizeof(_UA_HEADER)) { - ua = emalloc(ua_len + 1); - if ((ua_len = slprintf(ua, ua_len, _UA_HEADER, ua_str)) > 0) { - ua[ua_len] = 0; - smart_str_appendl(&req_buf, ua, ua_len); - } else { - php_error_docref(NULL, E_WARNING, "Cannot construct User-agent header"); - } - efree(ua); - } + if (((have_header & HTTP_HEADER_USER_AGENT) == 0) && ua_str && *ua_str) { + smart_str_appends(&req_buf, "User-Agent: "); + smart_str_append_header_value(&req_buf, ua_str, "User-Agent"); + smart_str_appends(&req_buf, "\r\n"); } if (user_headers) { diff --git a/ext/standard/tests/http/gh17976.phpt b/ext/standard/tests/http/gh17976.phpt new file mode 100644 index 0000000000000..52fd7705b91bc --- /dev/null +++ b/ext/standard/tests/http/gh17976.phpt @@ -0,0 +1,57 @@ +--TEST-- +GH-17976 (CRLF injection via from and user_agent INI settings in HTTP wrapper) +--INI-- +allow_url_fopen=1 +--FILE-- + ["tcp_nodelay" => true] + ]); + + $server = stream_socket_server( + "tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctxt); + phpt_notify_server_start($server); + + for ($i = 0; $i < 3; $i++) { + $conn = stream_socket_accept($server); + $result = fread($conn, 4096); + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n" . base64_encode($result)); + fclose($conn); + } +CODE; + +$clientCode = <<<'CODE' + // Test 1: from INI with CRLF + ini_set("from", "test\r\nInjected-From: evil"); + ini_set("user_agent", "clean_ua"); + $raw = base64_decode(file_get_contents("http://{{ ADDR }}/")); + echo (str_contains($raw, "Injected-From:") ? "FAIL" : "OK") . ": from INI\n"; + + // Test 2: user_agent INI with CRLF + ini_restore("from"); + ini_set("user_agent", "test\r\nInjected-UA: evil"); + $raw = base64_decode(file_get_contents("http://{{ ADDR }}/")); + echo (str_contains($raw, "Injected-UA:") ? "FAIL" : "OK") . ": user_agent INI\n"; + + // Test 3: user_agent context option with CRLF + ini_restore("user_agent"); + $ctx = stream_context_create(["http" => [ + "user_agent" => "test\nInjected-Ctx: evil" + ]]); + $raw = base64_decode(file_get_contents("http://{{ ADDR }}/", false, $ctx)); + echo (str_contains($raw, "Injected-Ctx:") ? "FAIL" : "OK") . ": user_agent context\n"; +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECTF-- +Warning: file_get_contents(): Header From value contains newline characters and has been truncated in %s on line %d +OK: from INI + +Warning: file_get_contents(): Header User-Agent value contains newline characters and has been truncated in %s on line %d +OK: user_agent INI + +Warning: file_get_contents(): Header User-Agent value contains newline characters and has been truncated in %s on line %d +OK: user_agent context