Commit 3dcdb9826be for php.net

commit 3dcdb9826bea199a27db67dd5ba6472e05a18a1d
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jul 17 07:08:58 2026 -0400

    ext/soap: truncate a user_agent containing newline characters (#22771)

    GH-17976 sanitized user_agent where the HTTP wrapper emits headers and left
    the SOAP client alone, so a value carrying CRLF still injects request headers
    through all three of its sources: the SoapClient user_agent option, the
    http.user_agent stream context option, and the ini setting. Resolve the three
    to one zend_string and emit it through the same truncate-and-warn check the
    HTTP wrapper applies.

    Closes GH-22771

diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c
index 43c2182ae47..22c18259d68 100644
--- a/ext/soap/php_http.c
+++ b/ext/soap/php_http.c
@@ -25,6 +25,23 @@ static zend_string *get_http_headers(php_stream *socketd);
 #define smart_str_append_const(str, const) \
 	smart_str_appendl(str,const,sizeof(const)-1)

+static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name)
+{
+	const char *src = ZSTR_VAL(value);
+	size_t len = ZSTR_LEN(value);
+	size_t i = 0;
+	while (i < len && src[i] != '\r' && src[i] != '\n') {
+		i++;
+	}
+	if (i < len) {
+		smart_str_appendl(dest, src, i);
+		php_error_docref(NULL, E_WARNING,
+			"Header %s value contains newline characters and has been truncated", header_name);
+	} else {
+		smart_str_append(dest, value);
+	}
+}
+
 /* Proxy HTTP Authentication */
 bool proxy_authentication(zval* this_ptr, smart_str* soap_headers)
 {
@@ -607,25 +624,25 @@ bool make_http_soap_request(
 			smart_str_append_const(&soap_headers, "\r\n"
 				"Connection: Keep-Alive\r\n");
 		}
+		zend_string *ua_str = NULL;
+
 		tmp = Z_CLIENT_USER_AGENT_P(this_ptr);
 		if (Z_TYPE_P(tmp) == IS_STRING) {
-			if (Z_STRLEN_P(tmp) > 0) {
-				smart_str_append_const(&soap_headers, "User-Agent: ");
-				smart_str_append(&soap_headers, Z_STR_P(tmp));
-				smart_str_append_const(&soap_headers, "\r\n");
-			}
+			ua_str = Z_STR_P(tmp);
 		} else if (context &&
 		           (tmp = php_stream_context_get_option(context, "http", "user_agent")) != NULL &&
 		           Z_TYPE_P(tmp) == IS_STRING) {
-			if (Z_STRLEN_P(tmp) > 0) {
+			ua_str = Z_STR_P(tmp);
+		} else if (FG(user_agent)) {
+			ua_str = FG(user_agent);
+		}
+
+		if (ua_str) {
+			if (ZSTR_LEN(ua_str) > 0) {
 				smart_str_append_const(&soap_headers, "User-Agent: ");
-				smart_str_append(&soap_headers, Z_STR_P(tmp));
+				soap_smart_str_append_header_value(&soap_headers, ua_str, "User-Agent");
 				smart_str_append_const(&soap_headers, "\r\n");
 			}
-		} else if (FG(user_agent)) {
-			smart_str_append_const(&soap_headers, "User-Agent: ");
-			smart_str_append(&soap_headers, FG(user_agent));
-			smart_str_append_const(&soap_headers, "\r\n");
 		} else {
 			smart_str_append_const(&soap_headers, "User-Agent: PHP-SOAP/"PHP_VERSION"\r\n");
 		}
diff --git a/ext/soap/tests/user_agent_header_injection.phpt b/ext/soap/tests/user_agent_header_injection.phpt
new file mode 100644
index 00000000000..979e1e57928
--- /dev/null
+++ b/ext/soap/tests/user_agent_header_injection.phpt
@@ -0,0 +1,47 @@
+--TEST--
+SoapClient must truncate a user_agent that contains newline characters
+--EXTENSIONS--
+soap
+--SKIPIF--
+<?php
+    if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
+        echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
+    }
+?>
+--FILE--
+<?php
+
+include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";
+
+$args = ["-d", "extension_dir=" . ini_get("extension_dir"), "-d", "extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX];
+if (php_ini_loaded_file()) {
+  $args[] = "-c";
+  $args[] = php_ini_loaded_file();
+}
+$code = <<<'PHP'
+$content = trim(file_get_contents("php://input")) . PHP_EOL;
+PHP;
+
+php_cli_server_start($code, null, $args);
+
+ini_set('user_agent', "evil\r\nX-Injected: yes");
+
+$client = new SoapClient(NULL, [
+  'location' => 'http://' . PHP_CLI_SERVER_ADDRESS,
+  'uri' => 'misc-uri',
+  'trace' => true,
+]);
+
+$client->__soapCall("foo", []);
+echo $client->__getLastRequestHeaders();
+
+?>
+--EXPECTF--
+Warning: SoapClient::__doRequest(): Header User-Agent value contains newline characters and has been truncated in %s on line %d
+POST / HTTP/1.1
+Host: localhost:%d
+Connection: Keep-Alive
+User-Agent: evil
+Content-Type: text/xml; charset=utf-8
+SOAPAction: "misc-uri#foo"
+Content-Length: %d