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
This commit is contained in:
Ilia Alshanetsky authored and GitHub committed 2026-07-17 07:08:58 -04:00
1 parent b5fba859ea
commit 3dcdb9826b
2 files changed
+75 -11

No files matched your search

+28 -11
View File
@@ -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 @@ try_again:
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");
}
@@ -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