From 2acaafc44065d246d622ab0283b6851044294af2 Mon Sep 17 00:00:00 2001 From: Gabriel Leite Bessa Date: Fri, 30 Oct 2026 03:41:38 -0300 Subject: [PATCH] Fix GH-8832: Honor protocol_version for proxy CONNECT requests The http stream wrapper always sent the CONNECT request used to tunnel HTTPS through a proxy as HTTP/1.0, ignoring the "protocol_version" context option. Proxies that only accept HTTP/1.1 reject it with 426 Upgrade Required. Use the "protocol_version" context option for the CONNECT request as well, and send the Host header required by HTTP/1.1 in that case. Without the option, CONNECT still uses HTTP/1.0. Co-Authored-By: Claude Opus 5.5 --- UPGRADING | 3 ++ ext/standard/http_fopen_wrapper.c | 13 +++++- .../tests/http/http_proxy_ssl_connect_04.phpt | 45 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/http/http_proxy_ssl_connect_04.phpt diff --git a/UPGRADING b/UPGRADING index 49d1ebc8258c..acb1eab8c5c3 100644 --- a/UPGRADING +++ b/UPGRADING @@ -107,6 +107,9 @@ PHP 8.7 UPGRADE NOTES rebuilding PHP with --enable-fd-setsize. On Windows, selecting on more than 64 pipe or file handles no longer crashes; it fails with a warning, as that path remains bounded by WaitForMultipleObjects(). + . The http stream wrapper now honors the "protocol_version" context option + for the CONNECT request sent to a proxy when tunneling HTTPS, and sends a + Host header in that case. Without the option, CONNECT still uses HTTP/1.0. ======================================== 6. New Functions diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 37844ec5e317..9a2e2b34cf31 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -571,7 +571,18 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, smart_str_append(&header, resource->host); smart_str_appendc(&header, ':'); smart_str_append_unsigned(&header, resource->port); - smart_str_appendl(&header, " HTTP/1.0\r\n", sizeof(" HTTP/1.0\r\n")-1); + if (context && (tmpzval = php_stream_context_get_option(context, "http", "protocol_version")) != NULL) { + smart_str_appends(&header, " HTTP/"); + smart_str_append_printf(&header, "%.1F", zval_get_double(tmpzval)); + /* HTTP/1.1 requires a Host header, which for CONNECT is the target authority */ + smart_str_appends(&header, "\r\nHost: "); + smart_str_append(&header, resource->host); + smart_str_appendc(&header, ':'); + smart_str_append_unsigned(&header, resource->port); + smart_str_appends(&header, "\r\n"); + } else { + smart_str_appendl(&header, " HTTP/1.0\r\n", sizeof(" HTTP/1.0\r\n")-1); + } /* check if we have Proxy-Authorization header */ if (context && (tmpzval = php_stream_context_get_option(context, "http", "header")) != NULL) { diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_04.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_04.phpt new file mode 100644 index 000000000000..72fcde55f655 --- /dev/null +++ b/ext/standard/tests/http/http_proxy_ssl_connect_04.phpt @@ -0,0 +1,45 @@ +--TEST-- +GH-8832 (HTTP proxy SSL CONNECT uses the http.protocol_version context option) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$host = parse_url($uri, PHP_URL_HOST); +$port = parse_url($uri, PHP_URL_PORT); + +$ctx = stream_context_create([ + 'http' => [ + 'proxy' => "tcp://$host:$port", + 'protocol_version' => 1.1, + 'header' => "Proxy-Authorization: Basic dXNlcjpwYXNz", + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]); +@file_get_contents("https://www.php.net/test", false, $ctx); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); +var_dump(strstr($request, "\r\n\r\n", true)); +?> +--EXPECT-- +string(96) "CONNECT www.php.net:443 HTTP/1.1 +Host: www.php.net:443 +Proxy-Authorization: Basic dXNlcjpwYXNz"