[php-src] master: ext/soap: truncate a user_agent containing newline characters (#22771)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-07-17T07:08:58-04:00

Commit: https://github.com/php/php-src/commit/3dcdb9826bea199a27db67dd5ba6472e05a18a1d
Raw diff: https://github.com/php/php-src/commit/3dcdb9826bea199a27db67dd5ba6472e05a18a1d.diff

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

Changed paths:
  A  ext/soap/tests/user_agent_header_injection.phpt
  M  ext/soap/php_http.c


Diff:

diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c
index 43c2182ae47a..22c18259d689 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 000000000000..979e1e579280
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.