Skip to content

Commit f6fe838

Browse files
committed
ext/soap: truncate header values containing newline characters.
Commit 3dcdb98 routed the user_agent option through a truncate-and-warn check, but the Content-Type context option, the soapaction and the cookie names and values reach the same request buffer unchecked, so a CR/LF in any of them still injects arbitrary request headers. Emit all of them through that same check. The helper now takes an explicit length so the char* soapaction can share it. Close GH-22781
1 parent d0f00cc commit f6fe838

5 files changed

Lines changed: 188 additions & 12 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ PHP NEWS
4040
. Fixed bug GH-22681 (Reflection*::__toString() truncates on null bytes).
4141
(DanielEScherzer)
4242

43+
- SOAP:
44+
. Fixed header injection through the Content-Type context option, the
45+
soapaction and the cookie names and values. (David Carlier)
46+
4347
- Sockets:
4448
. Fixed socket_set_option() validation error messages for UDP_SEGMENT and
4549
TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du)

ext/soap/php_http.c

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,24 @@ static zend_string *get_http_headers(php_stream *socketd);
2525
#define smart_str_append_const(str, const) \
2626
smart_str_appendl(str,const,sizeof(const)-1)
2727

28-
static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name)
28+
static void soap_smart_str_append_header_value_ex(smart_str *dest, const char *src, size_t len, const char *header_name)
2929
{
30-
const char *src = ZSTR_VAL(value);
31-
size_t len = ZSTR_LEN(value);
3230
size_t i = 0;
3331
while (i < len && src[i] != '\r' && src[i] != '\n') {
3432
i++;
3533
}
34+
smart_str_appendl(dest, src, i);
3635
if (i < len) {
37-
smart_str_appendl(dest, src, i);
3836
php_error_docref(NULL, E_WARNING,
3937
"Header %s value contains newline characters and has been truncated", header_name);
40-
} else {
41-
smart_str_append(dest, value);
4238
}
4339
}
4440

41+
static void soap_smart_str_append_header_value(smart_str *dest, const zend_string *value, const char *header_name)
42+
{
43+
soap_smart_str_append_header_value_ex(dest, ZSTR_VAL(value), ZSTR_LEN(value), header_name);
44+
}
45+
4546
/* Proxy HTTP Authentication */
4647
bool proxy_authentication(zval* this_ptr, smart_str* soap_headers)
4748
{
@@ -656,13 +657,13 @@ bool make_http_soap_request(
656657
Z_STRLEN_P(tmp) > 0
657658
) {
658659
smart_str_append_const(&soap_headers, "Content-Type: ");
659-
smart_str_append(&soap_headers, Z_STR_P(tmp));
660+
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type");
660661
} else {
661662
smart_str_append_const(&soap_headers, "Content-Type: application/soap+xml; charset=utf-8");
662663
}
663664
if (soapaction) {
664665
smart_str_append_const(&soap_headers,"; action=\"");
665-
smart_str_appends(&soap_headers, soapaction);
666+
soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction");
666667
smart_str_append_const(&soap_headers,"\"");
667668
}
668669
smart_str_append_const(&soap_headers,"\r\n");
@@ -673,14 +674,14 @@ bool make_http_soap_request(
673674
Z_STRLEN_P(tmp) > 0
674675
) {
675676
smart_str_append_const(&soap_headers, "Content-Type: ");
676-
smart_str_append(&soap_headers, Z_STR_P(tmp));
677+
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(tmp), "Content-Type");
677678
smart_str_append_const(&soap_headers, "\r\n");
678679
} else {
679680
smart_str_append_const(&soap_headers, "Content-Type: text/xml; charset=utf-8\r\n");
680681
}
681682
if (soapaction) {
682683
smart_str_append_const(&soap_headers, "SOAPAction: \"");
683-
smart_str_appends(&soap_headers, soapaction);
684+
soap_smart_str_append_header_value_ex(&soap_headers, soapaction, strlen(soapaction), "SOAPAction");
684685
smart_str_append_const(&soap_headers, "\"\r\n");
685686
}
686687
}
@@ -892,9 +893,9 @@ bool make_http_soap_request(
892893
smart_str_appends(&soap_headers, "; ");
893894
}
894895
first_cookie = false;
895-
smart_str_append(&soap_headers, key);
896+
soap_smart_str_append_header_value(&soap_headers, key, "Cookie");
896897
smart_str_appendc(&soap_headers, '=');
897-
smart_str_append(&soap_headers, Z_STR_P(value));
898+
soap_smart_str_append_header_value(&soap_headers, Z_STR_P(value), "Cookie");
898899
}
899900
}
900901
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
--TEST--
2+
SoapClient must truncate a content_type context option that contains newline characters
3+
--EXTENSIONS--
4+
soap
5+
--SKIPIF--
6+
<?php
7+
if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
8+
echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
14+
include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";
15+
16+
$args = ["-d", "extension_dir=" . ini_get("extension_dir"), "-d", "extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX];
17+
if (php_ini_loaded_file()) {
18+
$args[] = "-c";
19+
$args[] = php_ini_loaded_file();
20+
}
21+
$code = <<<'PHP'
22+
$content = trim(file_get_contents("php://input")) . PHP_EOL;
23+
PHP;
24+
25+
php_cli_server_start($code, null, $args);
26+
27+
$context = stream_context_create([
28+
'http' => ['content_type' => "text/xml\r\nX-Injected: yes"],
29+
]);
30+
31+
foreach ([SOAP_1_1, SOAP_1_2] as $version) {
32+
$client = new SoapClient(NULL, [
33+
'location' => 'http://' . PHP_CLI_SERVER_ADDRESS,
34+
'uri' => 'misc-uri',
35+
'trace' => true,
36+
'soap_version' => $version,
37+
'stream_context' => $context,
38+
]);
39+
40+
$client->__soapCall("foo", []);
41+
echo $client->__getLastRequestHeaders();
42+
}
43+
44+
?>
45+
--EXPECTF--
46+
Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d
47+
POST / HTTP/1.1
48+
Host: localhost:%d
49+
Connection: Keep-Alive
50+
User-Agent: PHP-SOAP/%s
51+
Content-Type: text/xml
52+
SOAPAction: "misc-uri#foo"
53+
Content-Length: %d
54+
55+
56+
Warning: SoapClient::__doRequest(): Header Content-Type value contains newline characters and has been truncated in %s on line %d
57+
POST / HTTP/1.1
58+
Host: localhost:%d
59+
Connection: Keep-Alive
60+
User-Agent: PHP-SOAP/%s
61+
Content-Type: text/xml; action="misc-uri#foo"
62+
Content-Length: %d
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
SoapClient must truncate a cookie name or value that contains newline characters
3+
--EXTENSIONS--
4+
soap
5+
--SKIPIF--
6+
<?php
7+
if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
8+
echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
14+
include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";
15+
16+
$args = ["-d", "extension_dir=" . ini_get("extension_dir"), "-d", "extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX];
17+
if (php_ini_loaded_file()) {
18+
$args[] = "-c";
19+
$args[] = php_ini_loaded_file();
20+
}
21+
$code = <<<'PHP'
22+
$content = trim(file_get_contents("php://input")) . PHP_EOL;
23+
PHP;
24+
25+
php_cli_server_start($code, null, $args);
26+
27+
$client = new SoapClient(NULL, [
28+
'location' => 'http://' . PHP_CLI_SERVER_ADDRESS,
29+
'uri' => 'misc-uri',
30+
'trace' => true,
31+
]);
32+
33+
$client->__setCookie("evil-value", "a\r\nX-Injected: yes");
34+
$client->__setCookie("evil-name\r\nX-Injected: yes", "b");
35+
$client->__setCookie("harmless", "c");
36+
37+
$client->__soapCall("foo", []);
38+
echo $client->__getLastRequestHeaders();
39+
40+
?>
41+
--EXPECTF--
42+
Warning: SoapClient::__doRequest(): Header Cookie value contains newline characters and has been truncated in %s on line %d
43+
44+
Warning: SoapClient::__doRequest(): Header Cookie value contains newline characters and has been truncated in %s on line %d
45+
POST / HTTP/1.1
46+
Host: localhost:%d
47+
Connection: Keep-Alive
48+
User-Agent: PHP-SOAP/%s
49+
Content-Type: text/xml; charset=utf-8
50+
SOAPAction: "misc-uri#foo"
51+
Content-Length: %d
52+
Cookie: evil-value=a; evil-name=b; harmless=c
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
SoapClient must truncate a soapaction that contains newline characters
3+
--EXTENSIONS--
4+
soap
5+
--SKIPIF--
6+
<?php
7+
if (!file_exists(__DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc")) {
8+
echo "skip sapi/cli/tests/php_cli_server.inc required but not found";
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
14+
include __DIR__ . "/../../../sapi/cli/tests/php_cli_server.inc";
15+
16+
$args = ["-d", "extension_dir=" . ini_get("extension_dir"), "-d", "extension=" . (substr(PHP_OS, 0, 3) == "WIN" ? "php_" : "") . "soap." . PHP_SHLIB_SUFFIX];
17+
if (php_ini_loaded_file()) {
18+
$args[] = "-c";
19+
$args[] = php_ini_loaded_file();
20+
}
21+
$code = <<<'PHP'
22+
$content = trim(file_get_contents("php://input")) . PHP_EOL;
23+
PHP;
24+
25+
php_cli_server_start($code, null, $args);
26+
27+
foreach ([SOAP_1_1, SOAP_1_2] as $version) {
28+
$client = new SoapClient(NULL, [
29+
'location' => 'http://' . PHP_CLI_SERVER_ADDRESS,
30+
'uri' => 'misc-uri',
31+
'trace' => true,
32+
'soap_version' => $version,
33+
]);
34+
35+
$client->__soapCall("foo", [], ['soapaction' => "an-action\r\nX-Injected: yes"]);
36+
echo $client->__getLastRequestHeaders();
37+
}
38+
39+
?>
40+
--EXPECTF--
41+
Warning: SoapClient::__doRequest(): Header SOAPAction value contains newline characters and has been truncated in %s on line %d
42+
POST / HTTP/1.1
43+
Host: localhost:%d
44+
Connection: Keep-Alive
45+
User-Agent: PHP-SOAP/%s
46+
Content-Type: text/xml; charset=utf-8
47+
SOAPAction: "an-action"
48+
Content-Length: %d
49+
50+
51+
Warning: SoapClient::__doRequest(): Header SOAPAction value contains newline characters and has been truncated in %s on line %d
52+
POST / HTTP/1.1
53+
Host: localhost:%d
54+
Connection: Keep-Alive
55+
User-Agent: PHP-SOAP/%s
56+
Content-Type: application/soap+xml; charset=utf-8; action="an-action"
57+
Content-Length: %d

0 commit comments

Comments
 (0)