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