CVE-2017-7189: Improper Input Validation (CWE-20)
value: main/streams/xp_socket.c in PHP 7.x before 2017-03-07 misparses fsockopen calls, such as by interpreting fsockopen('127.0.0.1:80', 443) as if the address/port were 127.0.0.1:80:443, which is later truncated to 127.0.0.1:80. This behavior has a security risk if the explicitly provided port number (i.e., 443 in this example) is hardcoded into an application as a security policy, but the hostname argument (i.e., 127.0.0.1:80 in this example) is obtained from untrusted input.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 7.5
Published: 2019-07-10T15:15Z
Repository: https://github.com/php/php-src
Language: C
Files changed: ext/standard/tests/streams/parseip-001.phpt, main/streams/xp_socket.c
Commit date: 2017-03-07 12:10:53 -0800
Commit message: Detect invalid port in xp_socket parse ip address

For historical reasons, fsockopen() accepts the port and hostname
separately: fsockopen('127.0.0.1', 80)

However, with the introdcution of stream transports in PHP 4.3,
it became possible to include the port in the hostname specifier:

fsockopen('127.0.0.1:80')
Or more formally: fsockopen('tcp://127.0.0.1:80')

Confusing results when these two forms are combined, however.
fsockopen('127.0.0.1:80', 443) results in fsockopen() attempting
to connect to '127.0.0.1:80:443' which any reasonable stack would
consider invalid.

Unfortunately, PHP parses the address looking for the first colon
(with special handling for IPv6, don't worry) and calls atoi()
from there.  atoi() in turn, simply stops parsing at the first
non-numeric character and returns the value so far.

The end result is that the explicitly supplied port is treated
as ignored garbage, rather than producing an error.

This diff replaces atoi() with strtol() and inspects the
stop character.  If additional "garbage" of any kind is found,
it fails and returns an error.

Diff: diff --git a/ext/standard/tests/streams/parseip-001.phpt b/ext/standard/tests/streams/parseip-001.phpt
new file mode 100644
index 00000000000..594756db6b7
--- /dev/null
+++ b/ext/standard/tests/streams/parseip-001.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Use of double-port in fsockopen()
+--FILE--
+<?php
+
+$try = [
+  '127.0.0.1:80',
+  'tcp://127.0.0.1:80',
+  '[::1]:80',
+  'tcp://[::1]:80',
+  'localhost:80',
+  'tcp://localhost:80',
+];
+
+foreach ($try as $addr) {
+  echo "== $addr ==\n";
+  var_dump(@fsockopen($addr, 81, $errno, $errstr), $errstr);
+}
+--EXPECTF--
+== 127.0.0.1:80 ==
+bool(false)
+string(41) "Failed to parse address "127.0.0.1:80:81""
+== tcp://127.0.0.1:80 ==
+bool(false)
+string(41) "Failed to parse address "127.0.0.1:80:81""
+== [::1]:80 ==
+bool(false)
+string(37) "Failed to parse address "[::1]:80:81""
+== tcp://[::1]:80 ==
+bool(false)
+string(37) "Failed to parse address "[::1]:80:81""
+== localhost:80 ==
+bool(false)
+string(41) "Failed to parse address "localhost:80:81""
+== tcp://localhost:80 ==
+bool(false)
+string(41) "Failed to parse address "localhost:80:81""
\ No newline at end of file
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index 701a993ccc1..3ff64787aa1 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -569,41 +569,48 @@ static inline char *parse_ip_address_ex(const char *str, size_t str_len, int *po
 {
 	char *colon;
 	char *host = NULL;
 
 #ifdef HAVE_IPV6
-	char *p;
-
 	if (*(str) == '[' && str_len > 1) {
 		/* IPV6 notation to specify raw address with port (i.e. [fe80::1]:80) */
-		p = memchr(str + 1, ']', str_len - 2);
+		char *p = memchr(str + 1, ']', str_len - 2), *e = NULL;
 		if (!p || *(p + 1) != ':') {
 			if (get_err) {
 				*err = strpprintf(0, "Failed to parse IPv6 address \"%s\"", str);
 			}
 			return NULL;
 		}
-		*portno = atoi(p + 2);
+		*portno = strtol(p + 2, &e, 10);
+		if (e && *e) {
+			if (get_err) {
+				*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+			}
+			return NULL;
+		}
 		return estrndup(str + 1, p - str - 1);
 	}
 #endif
+
 	if (str_len) {
 		colon = memchr(str, ':', str_len - 1);
 	} else {
 		colon = NULL;
 	}
+
 	if (colon) {
-		*portno = atoi(colon + 1);
-		host = estrndup(str, colon - str);
-	} else {
-		if (get_err) {
-			*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+		char *e = NULL;
+		*portno = strtol(colon + 1, &e, 10);
+		if (!e || !*e) {
+			return estrndup(str, colon - str);
 		}
-		return NULL;
 	}
 
-	return host;
+	if (get_err) {
+		*err = strpprintf(0, "Failed to parse address \"%s\"", str);
+	}
+	return NULL;
 }
 
 static inline char *parse_ip_address(php_stream_xport_param *xparam, int *portno)
 {
 	return parse_ip_address_ex(xparam->inputs.name, xparam->inputs.namelen, portno, xparam->want_errortext, &xparam->outputs.error_text);

Vulnerable code: 	char *p;

		p = memchr(str + 1, ']', str_len - 2);
		*portno = atoi(p + 2);
		*portno = atoi(colon + 1);
		host = estrndup(str, colon - str);
	} else {
		if (get_err) {
			*err = strpprintf(0, "Failed to parse address \"%s\"", str);
		return NULL;
	return host;
Fixed code: --TEST--
Use of double-port in fsockopen()
--FILE--
<?php

$try = [
  '127.0.0.1:80',
  'tcp://127.0.0.1:80',
  '[::1]:80',
  'tcp://[::1]:80',
  'localhost:80',
  'tcp://localhost:80',
];

foreach ($try as $addr) {
  echo "== $addr ==\n";
  var_dump(@fsockopen($addr, 81, $errno, $errstr), $errstr);
}
--EXPECTF--
== 127.0.0.1:80 ==
bool(false)
string(41) "Failed to parse address "127.0.0.1:80:81""
== tcp://127.0.0.1:80 ==
bool(false)
string(41) "Failed to parse address "127.0.0.1:80:81""
== [::1]:80 ==
bool(false)
string(37) "Failed to parse address "[::1]:80:81""
== tcp://[::1]:80 ==
bool(false)
string(37) "Failed to parse address "[::1]:80:81""
== localhost:80 ==
bool(false)
string(41) "Failed to parse address "localhost:80:81""
== tcp://localhost:80 ==
bool(false)
string(41) "Failed to parse address "localhost:80:81""
		char *p = memchr(str + 1, ']', str_len - 2), *e = NULL;
		*portno = strtol(p + 2, &e, 10);
		if (e && *e) {
			if (get_err) {
				*err = strpprintf(0, "Failed to parse address \"%s\"", str);
			}
			return NULL;
		}


		char *e = NULL;
		*portno = strtol(colon + 1, &e, 10);
		if (!e || !*e) {
			return estrndup(str, colon - str);
	if (get_err) {
		*err = strpprintf(0, "Failed to parse address \"%s\"", str);
	}
	return NULL;