From 9f082163aac857e003aeae3c8aec211a22da7bdd Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 21 Feb 2025 15:23:24 +0100 Subject: [PATCH] Fix bug #74796: Requests through http proxy set peer name This issue happens because http wrapper sets peer_name but then does not remove so it stays in the context. The fix removes the peer name from the context after enabling crypto. In addition to bug #74796, this also fixes bug #76196. In addition it should be a final fix for those SOAP bugs: bug #69783 bug #52913 bug #61463 --- ext/openssl/tests/bug74796.phpt | 175 ++++++++++++++++++++++++++++++ ext/standard/http_fopen_wrapper.c | 6 + main/streams/php_stream_context.h | 3 +- main/streams/streams.c | 14 +++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 ext/openssl/tests/bug74796.phpt diff --git a/ext/openssl/tests/bug74796.phpt b/ext/openssl/tests/bug74796.phpt new file mode 100644 index 0000000000000..a5c99e61697a8 --- /dev/null +++ b/ext/openssl/tests/bug74796.phpt @@ -0,0 +1,175 @@ +--TEST-- +Bug #74796: TLS encryption fails behind HTTP proxy +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'SNI_server_certs' => [ + "cs.php.net" => __DIR__ . "/sni_server_cs.pem", + "uk.php.net" => __DIR__ . "/sni_server_uk.pem", + "us.php.net" => __DIR__ . "/sni_server_us.pem" + ] + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $serverFlags, $ctx); + phpt_notify_server_start($server); + + for ($i=0; $i < 3; $i++) { + $conn = stream_socket_accept($server, 3); + fwrite($conn, "HTTP/1.0 200 OK\r\n\r\nHello from server $i"); + fclose($conn); + } + + phpt_wait(); +CODE; + +$proxyCode = <<<'CODE' + function parse_sni_from_client_hello($data) { + $sni = null; + + if (strlen($data) < 5 || ord($data[0]) != 0x16) return null; + + $session_id_len = ord($data[43]); + $ptr = 44 + $session_id_len; + + // Cipher suites length + $cipher_suites_len = (ord($data[$ptr]) << 8) | ord($data[$ptr+1]); + $ptr += 2 + $cipher_suites_len; + + // Compression methods length + $compression_methods_len = ord($data[$ptr]); + $ptr += 1 + $compression_methods_len; + + // Extensions length + if ($ptr + 2 > strlen($data)) return null; + $extensions_len = (ord($data[$ptr]) << 8) | ord($data[$ptr+1]); + $ptr += 2; + + $extensions_end = $ptr + $extensions_len; + + while ($ptr + 4 <= $extensions_end) { + $ext_type = (ord($data[$ptr]) << 8) | ord($data[$ptr+1]); + $ext_len = (ord($data[$ptr+2]) << 8) | ord($data[$ptr+3]); + $ptr += 4; + + if ($ext_type === 0x00) { // SNI extension + if ($ptr + 2 > strlen($data)) break; + $name_list_len = (ord($data[$ptr]) << 8) | ord($data[$ptr+1]); + $ptr += 2; + + if ($ptr + 3 > strlen($data)) break; + $name_type = ord($data[$ptr]); + $name_len = (ord($data[$ptr+1]) << 8) | ord($data[$ptr+2]); + $ptr += 3; + + if ($name_type === 0) { // host_name type + $sni = substr($data, $ptr, $name_len); + break; + } + } + + $ptr += $ext_len; + } + + return $sni; + } + + $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; + $server = stream_socket_server("tcp://127.0.0.1:0", $errornum, $errorstr, $flags); + phpt_notify_server_start($server); + + for ($i=0; $i < 3; $i++) { + $upstream = stream_socket_client("tcp://{{ ADDR }}", $errornum, $errorstr, 30, STREAM_CLIENT_CONNECT); + stream_set_blocking($upstream, false); + + $conn = stream_socket_accept($server); + stream_set_blocking($conn, true); + + // reading CONNECT request headers + while (($line = fgets($conn)) !== false) { + if (rtrim($line) === '') break; // empty line means end of headers + } + + // successful CONNECT response + fwrite($conn, "HTTP/1.0 200 Connection established\r\n\r\n"); + + // tunnel data + stream_set_blocking($conn, false); + $firstRead = true; + while (!feof($conn) && !feof($upstream)) { + $clientData = fread($conn, 8192); + if ($clientData !== false && $clientData !== '') { + if ($firstRead) { + $sni = parse_sni_from_client_hello($clientData); + if ($sni !== null) { + file_put_contents(__DIR__ . "/bug74796_proxy_sni.log", $sni . "\n", FILE_APPEND); + } + $firstRead = false; + } + fwrite($upstream, $clientData); + } + + $serverData = fread($upstream, 8192); + if ($serverData !== false && $serverData !== '') { + fwrite($conn, $serverData); + } + } + fclose($conn); + fclose($upstream); + phpt_wait(); + } +CODE; + +$clientCode = <<<'CODE' + $clientCtx = stream_context_create([ + 'ssl' => [ + 'cafile' => __DIR__ . '/sni_server_ca.pem', + 'verify_peer' => true, + 'verify_peer_name' => true, + ], + "http" => [ + "proxy" => "tcp://{{ ADDR }}" + ], + ]); + + // servers + $hosts = ["cs.php.net", "uk.php.net", "us.php.net"]; + foreach ($hosts as $host) { + var_dump(file_get_contents("https://$host/", false, $clientCtx)); + var_dump(stream_context_get_options($clientCtx)['ssl']['peer_name'] ?? null); + phpt_notify('proxy'); + } + + echo file_get_contents(__DIR__ . "/bug74796_proxy_sni.log"); + + phpt_notify('server'); +CODE; + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, [ + 'server' => $serverCode, + 'proxy' => $proxyCode, +]); +?> +--CLEAN-- + +--EXPECT-- +string(19) "Hello from server 0" +NULL +string(19) "Hello from server 1" +NULL +string(19) "Hello from server 2" +NULL +cs.php.net +uk.php.net +us.php.net diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index b4d065dd0b625..040ee4eabf78b 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -470,12 +470,14 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, if (stream && use_proxy && use_ssl) { smart_str header = {0}; + bool reset_ssl_peer_name = false; /* Set peer_name or name verification will try to use the proxy server name */ if (!context || (tmpzval = php_stream_context_get_option(context, "ssl", "peer_name")) == NULL) { ZVAL_STR_COPY(&ssl_proxy_peer_name, resource->host); php_stream_context_set_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name", &ssl_proxy_peer_name); zval_ptr_dtor(&ssl_proxy_peer_name); + reset_ssl_peer_name = true; } smart_str_appendl(&header, "CONNECT ", sizeof("CONNECT ")-1); @@ -572,6 +574,10 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, stream = NULL; } } + + if (reset_ssl_peer_name) { + php_stream_context_unset_option(PHP_STREAM_CONTEXT(stream), "ssl", "peer_name"); + } } php_stream_http_response_header_info_init(&header_info); diff --git a/main/streams/php_stream_context.h b/main/streams/php_stream_context.h index d4ebe29bc162e..56a1f53747116 100644 --- a/main/streams/php_stream_context.h +++ b/main/streams/php_stream_context.h @@ -59,7 +59,8 @@ PHPAPI zval *php_stream_context_get_option(php_stream_context *context, const char *wrappername, const char *optionname); PHPAPI void php_stream_context_set_option(php_stream_context *context, const char *wrappername, const char *optionname, zval *optionvalue); - +void php_stream_context_unset_option(php_stream_context *context, + const char *wrappername, const char *optionname); PHPAPI php_stream_notifier *php_stream_notification_alloc(void); PHPAPI void php_stream_notification_free(php_stream_notifier *notifier); END_EXTERN_C() diff --git a/main/streams/streams.c b/main/streams/streams.c index 4e0aaa53b443e..4f9c88e4774c4 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -2432,6 +2432,20 @@ PHPAPI void php_stream_context_set_option(php_stream_context *context, SEPARATE_ARRAY(wrapperhash); zend_hash_str_update(Z_ARRVAL_P(wrapperhash), optionname, strlen(optionname), optionvalue); } + +void php_stream_context_unset_option(php_stream_context *context, + const char *wrappername, const char *optionname) +{ + zval *wrapperhash; + + wrapperhash = zend_hash_str_find(Z_ARRVAL(context->options), wrappername, strlen(wrappername)); + if (NULL == wrapperhash) { + return; + } + SEPARATE_ARRAY(&context->options); + SEPARATE_ARRAY(wrapperhash); + zend_hash_str_del(Z_ARRVAL_P(wrapperhash), optionname, strlen(optionname)); +} /* }}} */ /* {{{ php_stream_dirent_alphasort */