From 5270e3ef36b66a1b5a5796e4d22ce43d50d18cf7 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 16 Jul 2026 13:08:37 -0400 Subject: [PATCH] ext/soap: truncate a user_agent containing newline characters 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 --- ext/soap/php_http.c | 39 ++++++++++----- .../tests/user_agent_header_injection.phpt | 47 +++++++++++++++++++ 2 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 ext/soap/tests/user_agent_header_injection.phpt diff --git a/ext/soap/php_http.c b/ext/soap/php_http.c index fe808404b526..10450004455a 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-- + +--FILE-- + '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