CVE-2017-7566: Server-Side Request Forgery (SSRF) (CWE-918)
value: MyBB before 1.8.11 allows remote attackers to bypass an SSRF protection mechanism.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.7
Published: 2017-04-06T16:59Z
Repository: https://github.com/mybb/mybb
Language: PHP
Files changed: admin/modules/config/smilies.php, inc/class_parser.php, inc/functions.php
Commit date: 2017-04-04 20:42:39 +0200
Commit message: MyBB 1.8.11

Diff: diff --git a/admin/modules/config/smilies.php b/admin/modules/config/smilies.php
index c8cc4312..ee711e59 100644
--- a/admin/modules/config/smilies.php
+++ b/admin/modules/config/smilies.php
@@ -407,12 +407,14 @@ if($mybb->input['action'] == "add_multiple")
 				{
 					$ext = get_extension($file);
 					$find = str_replace(".".$ext, "", $file);
 					$name = ucfirst($find);
 
-					$form_container->output_cell("<img src=\"../".$path.$file."\" alt=\"\" /><br /><small>{$file}</small>", array("class" => "align_center", "width" => 1));
-					$form_container->output_cell($form->generate_text_box("name[{$file}]", $name, array('id' => 'name', 'style' => 'width: 98%')));
+					$file = htmlspecialchars_uni($file);
+
+					$form_container->output_cell("<img src=\"../".htmlspecialchars_uni($path).$file."\" alt=\"\" /><br /><small>{$file}</small>", array("class" => "align_center", "width" => 1));
+					$form_container->output_cell($form->generate_text_box("name[{$file}]", htmlspecialchars_uni($name), array('id' => 'name', 'style' => 'width: 98%')));
 					$form_container->output_cell($form->generate_text_box("find[{$file}]", ":".$find.":", array('id' => 'find', 'style' => 'width: 95%')));
 					$form_container->output_cell($form->generate_check_box("include[{$file}]", 1, "", array('checked' => 1)), array("class" => "align_center"));
 					$form_container->construct_row();
 				}
 
@@ -643,15 +645,15 @@ if($mybb->input['action'] == "mass_edit")
 	while($smilie = $db->fetch_array($query))
 	{
 		$smilie['image'] = str_replace("{theme}", "images", $smilie['image']);
 		if(my_validate_url($smilie['image'], true))
 		{
-			$image = $smilie['image'];
+			$image = htmlspecialchars_uni($smilie['image']);
 		}
 		else
 		{
-			$image = "../".$smilie['image'];
+			$image = "../".htmlspecialchars_uni($smilie['image']);
 		}
 
 		$form_container->output_cell("<img src=\"{$image}\" alt=\"\" />", array("class" => "align_center", "width" => 1));
 		$form_container->output_cell($form->generate_text_box("name[{$smilie['sid']}]", $smilie['name'], array('id' => 'name', 'style' => 'width: 98%')));
 		$form_container->output_cell($form->generate_text_area("find[{$smilie['sid']}]", $smilie['find'], array('id' => 'find', 'style' => 'width: 95%')));
@@ -726,15 +728,15 @@ if(!$mybb->input['action'])
 	while($smilie = $db->fetch_array($query))
 	{
 		$smilie['image'] = str_replace("{theme}", "images", $smilie['image']);
 		if(my_validate_url($smilie['image'], true))
 		{
-			$image = $smilie['image'];
+			$image = htmlspecialchars_uni($smilie['image']);
 		}
 		else
 		{
-			$image = "../".$smilie['image'];
+			$image = "../".htmlspecialchars_uni($smilie['image']);
 		}
 
 		$table->construct_cell("<img src=\"{$image}\" alt=\"\" class=\"smilie smilie_{$smilie['sid']}\" />", array("class" => "align_center"));
 		$table->construct_cell(htmlspecialchars_uni($smilie['name']));
 		$table->construct_cell(nl2br(htmlspecialchars_uni($smilie['find'])));
diff --git a/inc/class_parser.php b/inc/class_parser.php
index 1c74e268..22c70d79 100644
--- a/inc/class_parser.php
+++ b/inc/class_parser.php
@@ -315,14 +315,14 @@ class postParser
 			++$callback_count;
 		}
 
 		if($mybb->settings['allowemailmycode'] == 1)
 		{
-			$callback_mycode['email_simple']['regex'] = "#\[email\](.*?)\[/email\]#i";
+			$callback_mycode['email_simple']['regex'] = "#\[email\]((?:[a-zA-Z0-9-_\+\.]+?)@[a-zA-Z0-9-]+\.[a-zA-Z0-9\.-]+(?:\?.*?)?)\[/email\]#i";
 			$callback_mycode['email_simple']['replacement'] = array($this, 'mycode_parse_email_callback');
 
-			$callback_mycode['email_complex']['regex'] = "#\[email=(.*?)\](.*?)\[/email\]#i";
+			$callback_mycode['email_complex']['regex'] = "#\[email=((?:[a-zA-Z0-9-_\+\.]+?)@[a-zA-Z0-9-]+\.[a-zA-Z0-9\.-]+(?:\?.*?)?)\](.*?)\[/email\]#i";
 			$callback_mycode['email_complex']['replacement'] = array($this, 'mycode_parse_email_callback');
 
 			++$callback_count;
 		}
 
diff --git a/inc/functions.php b/inc/functions.php
index 6bd3e025..54668145 100644
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -6776,48 +6776,59 @@ function build_timezone_select($name, $selected=0, $short=false)
  */
 function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 {
 	global $mybb, $config;
 
+	if(!my_validate_url($url, true))
+	{
+		return false;
+	}
+
 	$url_components = @parse_url($url);
 
+	if(!isset($url_components['scheme']))
+	{
+		$url_components['scheme'] = 'https';
+	}
+	if(!isset($url_components['port']))
+	{
+		$url_components['port'] = $url_components['scheme'] == 'https' ? 443 : 80;
+	}
+
 	if(
 		!$url_components ||
 		empty($url_components['host']) ||
 		(!empty($url_components['scheme']) && !in_array($url_components['scheme'], array('http', 'https'))) ||
-		(!empty($url_components['port']) && !in_array($url_components['port'], array(80, 8080, 443))) ||
+		(!in_array($url_components['port'], array(80, 8080, 443))) ||
 		(!empty($config['disallowed_remote_hosts']) && in_array($url_components['host'], $config['disallowed_remote_hosts']))
 	)
 	{
 		return false;
 	}
 
+	$addresses = get_ip_by_hostname($url_components['host']);
+	$destination_address = $addresses[0];
+
 	if(!empty($config['disallowed_remote_addresses']))
 	{
-		$addresses = gethostbynamel($url_components['host']);
-		if($addresses)
+		foreach($config['disallowed_remote_addresses'] as $disallowed_address)
 		{
-			foreach($config['disallowed_remote_addresses'] as $disallowed_address)
+			$ip_range = fetch_ip_range($disallowed_address);
+
+			$packed_address = my_inet_pton($destination_address);
+
+			if(is_array($ip_range))
 			{
-				$ip_range = fetch_ip_range($disallowed_address);
-				foreach($addresses as $address)
+				if(strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0)
 				{
-					$packed_address = my_inet_pton($address);
-
-					if(is_array($ip_range))
-					{
-						if(strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0)
-						{
-							return false;
-						}
-					}
-					elseif($address == $disallowed_address)
-					{
-						return false;
-					}
+					return false;
 				}
 			}
+			elseif($destination_address == $disallowed_address)
+			{
+				return false;
+			}
 		}
 	}
 
 	$post_body = '';
 	if(!empty($post_data))
@@ -6829,36 +6840,59 @@ function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 		$post_body = ltrim($post_body, '&');
 	}
 
 	if(function_exists("curl_init"))
 	{
-		$can_followlocation = @ini_get('open_basedir') === '' && !$mybb->safemode;
-
-		$request_header = $max_redirects != 0 && !$can_followlocation;
+		$fetch_header = $max_redirects > 0;
 
 		$ch = curl_init();
-		curl_setopt($ch, CURLOPT_URL, $url);
-		curl_setopt($ch, CURLOPT_HEADER, $request_header);
-		curl_setopt($ch, CURLOPT_TIMEOUT, 10);
-		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
-		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
 
-		if($max_redirects != 0 && $can_followlocation)
+		$curlopt = array(
+			CURLOPT_URL => $url,
+			CURLOPT_HEADER => $fetch_header,
+			CURLOPT_TIMEOUT => 10,
+			CURLOPT_RETURNTRANSFER => 1,
+			CURLOPT_FOLLOWLOCATION => 0,
+		);
+
+		if($ca_bundle_path = get_ca_bundle_path())
 		{
-			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
-			curl_setopt($ch, CURLOPT_MAXREDIRS, $max_redirects);
+			$curlopt[CURLOPT_SSL_VERIFYPEER] = 1;
+			$curlopt[CURLOPT_CAINFO] = $ca_bundle_path;
+		}
+		else
+		{
+			$curlopt[CURLOPT_SSL_VERIFYPEER] = 0;
+		}
+
+		$curl_version_info = curl_version();
+		$curl_version = $curl_version_info['version'];
+
+		if(version_compare($curl_version, '7.0.7', '>=') && version_compare($curl_version, '7.49', '>='))
+		{
+			$curlopt[CURLOPT_CONNECT_TO] = array(
+				$url_components['host'].':'.$url_components['port'].':'.$destination_address
+			);
+		}
+		elseif(version_compare(PHP_VERSION, '5.5', '>='))
+		{
+			$curlopt[CURLOPT_RESOLVE] = array(
+				$url_components['host'].':'.$url_components['port'].':'.$destination_address
+			);
 		}
 
 		if(!empty($post_body))
 		{
-			curl_setopt($ch, CURLOPT_POST, 1);
-			curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body);
+			$curlopt[CURLOPT_POST] = 1;
+			$curlopt[CURLOPT_POSTFIELDS] = $post_body;
 		}
 
+		curl_setopt_array($ch, $curlopt);
+
 		$response = curl_exec($ch);
 
-		if($request_header)
+		if($fetch_header)
 		{
 			$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
 			$header = substr($response, 0, $header_size);
 			$body = substr($response, $header_size);
 
@@ -6884,14 +6918,10 @@ function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 		curl_close($ch);
 		return $data;
 	}
 	else if(function_exists("fsockopen"))
 	{
-		if(!isset($url_components['port']))
-		{
-			$url_components['port'] = 80;
-		}
 		if(!isset($url_components['path']))
 		{
 			$url_components['path'] = "/";
 		}
 		if(isset($url_components['query']))
@@ -6908,11 +6938,40 @@ function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 			{
 				$url_components['port'] = 443;
 			}
 		}
 
-		$fp = @fsockopen($scheme.$url_components['host'], $url_components['port'], $error_no, $error, 10);
+		if(function_exists('stream_context_create'))
+		{
+			if($url_components['scheme'] == 'https' && $ca_bundle_path = get_ca_bundle_path())
+			{
+				$context = stream_context_create([
+					'ssl' => [
+						'verify_peer' => true,
+						'verify_peer_name' => true,
+						'peer_name' => $url_components['host'],
+						'cafile' => $ca_bundle_path,
+					],
+				]);
+			}
+			else
+			{
+				$context = stream_context_create([
+					'ssl' => [
+						'verify_peer' => false,
+						'verify_peer_name' => false,
+					],
+				]);
+			}
+
+			$fp = @stream_socket_client($scheme.$destination_address.':'.(int)$url_components['port'], $error_no, $error, 10, STREAM_CLIENT_CONNECT, $context);
+		}
+		else
+		{
+			$fp = @fsockopen($scheme.$url_components['host'], (int)$url_components['port'], $error_no, $error, 10);
+		}
+
 		@stream_set_timeout($fp, 10);
 		if(!$fp)
 		{
 			return false;
 		}
@@ -6960,11 +7019,11 @@ function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 
 		$header = $data[0];
 		$status_line = current(explode("\n\n", $header, 1));
 		$body = $data[1];
 
-		if($max_redirects != 0 && (strstr($status_line, ' 301 ') || strstr($status_line, ' 302 ')))
+		if($max_redirects > 0 && (strstr($status_line, ' 301 ') || strstr($status_line, ' 302 ')))
 		{
 			preg_match('/Location:(.*?)(?:\n|$)/', $header, $matches);
 
 			if($matches)
 			{
@@ -6976,20 +7035,62 @@ function fetch_remote_file($url, $post_data=array(), $max_redirects=20)
 			$data = $body;
 		}
 
 		return $data;
 	}
-	else if(empty($post_data))
-	{
-		return @implode("", @file($url));
-	}
 	else
 	{
 		return false;
 	}
 }
 
+/**
+ * Resolves a hostname into a set of IP addresses.
+ *
+ * @param string $hostname The hostname to be resolved
+ * @return array|bool The resulting IP addresses. False on failure
+ */
+function get_ip_by_hostname($hostname)
+{
+	$addresses = gethostbynamel($hostname);
+
+	if(!$addresses)
+	{
+		$result_set = dns_get_record($hostname, DNS_A | DNS_AAAA);
+
+		if($result_set)
+		{
+			$addresses = array_column($result_set, 'ip');
+		}
+		else
+		{
+			return false;
+		}
+	}
+
+	return $addresses;
+}
+
+/**
+ * Returns the location of the CA bundle defined in the PHP configuration.
+ *
+ * @return string|bool The location of the CA bundle, false if not set
+ */
+function get_ca_bundle_path()
+{
+	if($path = ini_get('openssl.cafile'))
+	{
+		return $path;
+	}
+	if($path = ini_get('curl.cainfo'))
+	{
+		return $path;
+	}
+
+	return false;
+}
+
 /**
  * Checks if a particular user is a super administrator.
  *
  * @param int $uid The user ID to check against the list of super admins
  * @return boolean True if a super admin, false if not

Vulnerable code: 					$form_container->output_cell("<img src=\"../".$path.$file."\" alt=\"\" /><br /><small>{$file}</small>", array("class" => "align_center", "width" => 1));
					$form_container->output_cell($form->generate_text_box("name[{$file}]", $name, array('id' => 'name', 'style' => 'width: 98%')));
			$image = $smilie['image'];
			$image = "../".$smilie['image'];
			$image = $smilie['image'];
			$image = "../".$smilie['image'];
			$callback_mycode['email_simple']['regex'] = "#\[email\](.*?)\[/email\]#i";
			$callback_mycode['email_complex']['regex'] = "#\[email=(.*?)\](.*?)\[/email\]#i";
		(!empty($url_components['port']) && !in_array($url_components['port'], array(80, 8080, 443))) ||
		$addresses = gethostbynamel($url_components['host']);
		if($addresses)
			foreach($config['disallowed_remote_addresses'] as $disallowed_address)
				$ip_range = fetch_ip_range($disallowed_address);
				foreach($addresses as $address)
					$packed_address = my_inet_pton($address);

					if(is_array($ip_range))
					{
						if(strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0)
						{
							return false;
						}
					}
					elseif($address == $disallowed_address)
					{
						return false;
					}
		$can_followlocation = @ini_get('open_basedir') === '' && !$mybb->safemode;

		$request_header = $max_redirects != 0 && !$can_followlocation;
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_HEADER, $request_header);
		curl_setopt($ch, CURLOPT_TIMEOUT, 10);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
		if($max_redirects != 0 && $can_followlocation)
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
			curl_setopt($ch, CURLOPT_MAXREDIRS, $max_redirects);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $post_body);
		if($request_header)
		if(!isset($url_components['port']))
		{
			$url_components['port'] = 80;
		}
		$fp = @fsockopen($scheme.$url_components['host'], $url_components['port'], $error_no, $error, 10);
		if($max_redirects != 0 && (strstr($status_line, ' 301 ') || strstr($status_line, ' 302 ')))
	else if(empty($post_data))
	{
		return @implode("", @file($url));
	}
Fixed code: 					$file = htmlspecialchars_uni($file);

					$form_container->output_cell("<img src=\"../".htmlspecialchars_uni($path).$file."\" alt=\"\" /><br /><small>{$file}</small>", array("class" => "align_center", "width" => 1));
					$form_container->output_cell($form->generate_text_box("name[{$file}]", htmlspecialchars_uni($name), array('id' => 'name', 'style' => 'width: 98%')));
			$image = htmlspecialchars_uni($smilie['image']);
			$image = "../".htmlspecialchars_uni($smilie['image']);
			$image = htmlspecialchars_uni($smilie['image']);
			$image = "../".htmlspecialchars_uni($smilie['image']);
			$callback_mycode['email_simple']['regex'] = "#\[email\]((?:[a-zA-Z0-9-_\+\.]+?)@[a-zA-Z0-9-]+\.[a-zA-Z0-9\.-]+(?:\?.*?)?)\[/email\]#i";
			$callback_mycode['email_complex']['regex'] = "#\[email=((?:[a-zA-Z0-9-_\+\.]+?)@[a-zA-Z0-9-]+\.[a-zA-Z0-9\.-]+(?:\?.*?)?)\](.*?)\[/email\]#i";
	if(!my_validate_url($url, true))
	{
		return false;
	}

	if(!isset($url_components['scheme']))
	{
		$url_components['scheme'] = 'https';
	}
	if(!isset($url_components['port']))
	{
		$url_components['port'] = $url_components['scheme'] == 'https' ? 443 : 80;
	}

		(!in_array($url_components['port'], array(80, 8080, 443))) ||
	$addresses = get_ip_by_hostname($url_components['host']);
	$destination_address = $addresses[0];

		foreach($config['disallowed_remote_addresses'] as $disallowed_address)
			$ip_range = fetch_ip_range($disallowed_address);

			$packed_address = my_inet_pton($destination_address);

			if(is_array($ip_range))
				if(strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0)
					return false;
			elseif($destination_address == $disallowed_address)
			{
				return false;
			}
		$fetch_header = $max_redirects > 0;
		$curlopt = array(
			CURLOPT_URL => $url,
			CURLOPT_HEADER => $fetch_header,
			CURLOPT_TIMEOUT => 10,
			CURLOPT_RETURNTRANSFER => 1,
			CURLOPT_FOLLOWLOCATION => 0,
		);

		if($ca_bundle_path = get_ca_bundle_path())
			$curlopt[CURLOPT_SSL_VERIFYPEER] = 1;
			$curlopt[CURLOPT_CAINFO] = $ca_bundle_path;
		}
		else
		{
			$curlopt[CURLOPT_SSL_VERIFYPEER] = 0;
		}

		$curl_version_info = curl_version();
		$curl_version = $curl_version_info['version'];

		if(version_compare($curl_version, '7.0.7', '>=') && version_compare($curl_version, '7.49', '>='))
		{
			$curlopt[CURLOPT_CONNECT_TO] = array(
				$url_components['host'].':'.$url_components['port'].':'.$destination_address
			);
		}
		elseif(version_compare(PHP_VERSION, '5.5', '>='))
		{
			$curlopt[CURLOPT_RESOLVE] = array(
				$url_components['host'].':'.$url_components['port'].':'.$destination_address
			);
			$curlopt[CURLOPT_POST] = 1;
			$curlopt[CURLOPT_POSTFIELDS] = $post_body;
		curl_setopt_array($ch, $curlopt);

		if($fetch_header)
		if(function_exists('stream_context_create'))
		{
			if($url_components['scheme'] == 'https' && $ca_bundle_path = get_ca_bundle_path())
			{
				$context = stream_context_create([
					'ssl' => [
						'verify_peer' => true,
						'verify_peer_name' => true,
						'peer_name' => $url_components['host'],
						'cafile' => $ca_bundle_path,
					],
				]);
			}
			else
			{
				$context = stream_context_create([
					'ssl' => [
						'verify_peer' => false,
						'verify_peer_name' => false,
					],
				]);
			}

			$fp = @stream_socket_client($scheme.$destination_address.':'.(int)$url_components['port'], $error_no, $error, 10, STREAM_CLIENT_CONNECT, $context);
		}
		else
		{
			$fp = @fsockopen($scheme.$url_components['host'], (int)$url_components['port'], $error_no, $error, 10);
		}

		if($max_redirects > 0 && (strstr($status_line, ' 301 ') || strstr($status_line, ' 302 ')))
/**
 * Resolves a hostname into a set of IP addresses.
 *
 * @param string $hostname The hostname to be resolved
 * @return array|bool The resulting IP addresses. False on failure
 */
function get_ip_by_hostname($hostname)
{
	$addresses = gethostbynamel($hostname);

	if(!$addresses)
	{
		$result_set = dns_get_record($hostname, DNS_A | DNS_AAAA);

		if($result_set)
		{
			$addresses = array_column($result_set, 'ip');
		}
		else
		{
			return false;
		}
	}

	return $addresses;
}

/**
 * Returns the location of the CA bundle defined in the PHP configuration.
 *
 * @return string|bool The location of the CA bundle, false if not set
 */
function get_ca_bundle_path()
{
	if($path = ini_get('openssl.cafile'))
	{
		return $path;
	}
	if($path = ini_get('curl.cainfo'))
	{
		return $path;
	}

	return false;
}