From 2c25a64dd38f9274d9d0927cf4496f30b3086580 Mon Sep 17 00:00:00 2001 From: rodneyrehm Date: Sun, 18 Dec 2011 22:21:49 +0000 Subject: [PATCH] - added Smarty_Security::isTrustedUri() and Smarty_Security::$trusted_uri to validate remote resource calls through {fetch} and {html_image} (Forum Topic 20627) --- change_log.txt | 2 + libs/plugins/function.fetch.php | 340 +++++++++++++-------------- libs/plugins/function.html_image.php | 43 +++- libs/sysplugins/smarty_security.php | 34 ++- 4 files changed, 236 insertions(+), 183 deletions(-) diff --git a/change_log.txt b/change_log.txt index 93442adb..013bd542 100644 --- a/change_log.txt +++ b/change_log.txt @@ -6,6 +6,8 @@ - bugfix unregisterObject() raised notice when object to unregister did not exist - changed internals to use Smarty::$_MBSTRING ($_CHARSET, $_DATE_FORMAT) for better unit testing - added Smarty::$_UTF8_MODIFIER for proper PCRE charset handling (Forum Topic 20452) +- added Smarty_Security::isTrustedUri() and Smarty_Security::$trusted_uri to validate + remote resource calls through {fetch} and {html_image} (Forum Topic 20627) 17.12.2011 - improvement of compiling speed by new handling of plain text blocks in the lexer/parser (issue #68) diff --git a/libs/plugins/function.fetch.php b/libs/plugins/function.fetch.php index cde98d2e..eca1182d 100644 --- a/libs/plugins/function.fetch.php +++ b/libs/plugins/function.fetch.php @@ -26,188 +26,186 @@ function smarty_function_fetch($params, $template) trigger_error("[plugin] fetch parameter 'file' cannot be empty",E_USER_NOTICE); return; } + + // strip file protocol + if (stripos($params['file'], 'file://') === 0) { + $params['file'] = substr($params['file'], 7); + } + + $protocol = strpos($params['file'], '://'); + if ($protocol !== false) { + $protocol = strtolower(substr($params['file'], 0, $protocol)); + } + + if (isset($template->smarty->security_policy)) { + if ($protocol) { + // remote resource (or php stream, …) + if(!$template->smarty->security_policy->isTrustedUri($params['file'])) { + return; + } + } else { + // local file + if(!$template->smarty->security_policy->isTrustedResourceDir($params['file'])) { + return; + } + } + } $content = ''; - if (isset($template->smarty->security_policy) && !preg_match('!^(http|ftp)://!i', $params['file'])) { - if(!$template->smarty->security_policy->isTrustedResourceDir($params['file'])) { - return; - } - - // fetch the file - if($fp = @fopen($params['file'],'r')) { - while(!feof($fp)) { - $content .= fgets ($fp,4096); + if ($protocol == 'http') { + // http fetch + if($uri_parts = parse_url($params['file'])) { + // set defaults + $host = $server_name = $uri_parts['host']; + $timeout = 30; + $accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*"; + $agent = "Smarty Template Engine ". Smarty::SMARTY_VERSION; + $referer = ""; + $uri = !empty($uri_parts['path']) ? $uri_parts['path'] : '/'; + $uri .= !empty($uri_parts['query']) ? '?' . $uri_parts['query'] : ''; + $_is_proxy = false; + if(empty($uri_parts['port'])) { + $port = 80; + } else { + $port = $uri_parts['port']; + } + if(!empty($uri_parts['user'])) { + $user = $uri_parts['user']; + } + if(!empty($uri_parts['pass'])) { + $pass = $uri_parts['pass']; + } + // loop through parameters, setup headers + foreach($params as $param_key => $param_value) { + switch($param_key) { + case "file": + case "assign": + case "assign_headers": + break; + case "user": + if(!empty($param_value)) { + $user = $param_value; + } + break; + case "pass": + if(!empty($param_value)) { + $pass = $param_value; + } + break; + case "accept": + if(!empty($param_value)) { + $accept = $param_value; + } + break; + case "header": + if(!empty($param_value)) { + if(!preg_match('![\w\d-]+: .+!',$param_value)) { + trigger_error("[plugin] invalid header format '".$param_value."'",E_USER_NOTICE); + return; + } else { + $extra_headers[] = $param_value; + } + } + break; + case "proxy_host": + if(!empty($param_value)) { + $proxy_host = $param_value; + } + break; + case "proxy_port": + if(!preg_match('!\D!', $param_value)) { + $proxy_port = (int) $param_value; + } else { + trigger_error("[plugin] invalid value for attribute '".$param_key."'",E_USER_NOTICE); + return; + } + break; + case "agent": + if(!empty($param_value)) { + $agent = $param_value; + } + break; + case "referer": + if(!empty($param_value)) { + $referer = $param_value; + } + break; + case "timeout": + if(!preg_match('!\D!', $param_value)) { + $timeout = (int) $param_value; + } else { + trigger_error("[plugin] invalid value for attribute '".$param_key."'",E_USER_NOTICE); + return; + } + break; + default: + trigger_error("[plugin] unrecognized attribute '".$param_key."'",E_USER_NOTICE); + return; + } + } + if(!empty($proxy_host) && !empty($proxy_port)) { + $_is_proxy = true; + $fp = fsockopen($proxy_host,$proxy_port,$errno,$errstr,$timeout); + } else { + $fp = fsockopen($server_name,$port,$errno,$errstr,$timeout); + } + + if(!$fp) { + trigger_error("[plugin] unable to fetch: $errstr ($errno)",E_USER_NOTICE); + return; + } else { + if($_is_proxy) { + fputs($fp, 'GET ' . $params['file'] . " HTTP/1.0\r\n"); + } else { + fputs($fp, "GET $uri HTTP/1.0\r\n"); + } + if(!empty($host)) { + fputs($fp, "Host: $host\r\n"); + } + if(!empty($accept)) { + fputs($fp, "Accept: $accept\r\n"); + } + if(!empty($agent)) { + fputs($fp, "User-Agent: $agent\r\n"); + } + if(!empty($referer)) { + fputs($fp, "Referer: $referer\r\n"); + } + if(isset($extra_headers) && is_array($extra_headers)) { + foreach($extra_headers as $curr_header) { + fputs($fp, $curr_header."\r\n"); + } + } + if(!empty($user) && !empty($pass)) { + fputs($fp, "Authorization: BASIC ".base64_encode("$user:$pass")."\r\n"); + } + + fputs($fp, "\r\n"); + while(!feof($fp)) { + $content .= fgets($fp,4096); + } + fclose($fp); + $csplit = preg_split("!\r\n\r\n!",$content,2); + + $content = $csplit[1]; + + if(!empty($params['assign_headers'])) { + $template->assign($params['assign_headers'],preg_split("!\r\n!",$csplit[0])); + } } - fclose($fp); } else { - trigger_error('[plugin] fetch cannot read file \'' . $params['file'] . '\'',E_USER_NOTICE); + trigger_error("[plugin fetch] unable to parse URL, check syntax",E_USER_NOTICE); return; } } else { - // not a local file - if(preg_match('!^http://!i',$params['file'])) { - // http fetch - if($uri_parts = parse_url($params['file'])) { - // set defaults - $host = $server_name = $uri_parts['host']; - $timeout = 30; - $accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*"; - $agent = "Smarty Template Engine ". Smarty::SMARTY_VERSION; - $referer = ""; - $uri = !empty($uri_parts['path']) ? $uri_parts['path'] : '/'; - $uri .= !empty($uri_parts['query']) ? '?' . $uri_parts['query'] : ''; - $_is_proxy = false; - if(empty($uri_parts['port'])) { - $port = 80; - } else { - $port = $uri_parts['port']; - } - if(!empty($uri_parts['user'])) { - $user = $uri_parts['user']; - } - if(!empty($uri_parts['pass'])) { - $pass = $uri_parts['pass']; - } - // loop through parameters, setup headers - foreach($params as $param_key => $param_value) { - switch($param_key) { - case "file": - case "assign": - case "assign_headers": - break; - case "user": - if(!empty($param_value)) { - $user = $param_value; - } - break; - case "pass": - if(!empty($param_value)) { - $pass = $param_value; - } - break; - case "accept": - if(!empty($param_value)) { - $accept = $param_value; - } - break; - case "header": - if(!empty($param_value)) { - if(!preg_match('![\w\d-]+: .+!',$param_value)) { - trigger_error("[plugin] invalid header format '".$param_value."'",E_USER_NOTICE); - return; - } else { - $extra_headers[] = $param_value; - } - } - break; - case "proxy_host": - if(!empty($param_value)) { - $proxy_host = $param_value; - } - break; - case "proxy_port": - if(!preg_match('!\D!', $param_value)) { - $proxy_port = (int) $param_value; - } else { - trigger_error("[plugin] invalid value for attribute '".$param_key."'",E_USER_NOTICE); - return; - } - break; - case "agent": - if(!empty($param_value)) { - $agent = $param_value; - } - break; - case "referer": - if(!empty($param_value)) { - $referer = $param_value; - } - break; - case "timeout": - if(!preg_match('!\D!', $param_value)) { - $timeout = (int) $param_value; - } else { - trigger_error("[plugin] invalid value for attribute '".$param_key."'",E_USER_NOTICE); - return; - } - break; - default: - trigger_error("[plugin] unrecognized attribute '".$param_key."'",E_USER_NOTICE); - return; - } - } - if(!empty($proxy_host) && !empty($proxy_port)) { - $_is_proxy = true; - $fp = fsockopen($proxy_host,$proxy_port,$errno,$errstr,$timeout); - } else { - $fp = fsockopen($server_name,$port,$errno,$errstr,$timeout); - } - - if(!$fp) { - trigger_error("[plugin] unable to fetch: $errstr ($errno)",E_USER_NOTICE); - return; - } else { - if($_is_proxy) { - fputs($fp, 'GET ' . $params['file'] . " HTTP/1.0\r\n"); - } else { - fputs($fp, "GET $uri HTTP/1.0\r\n"); - } - if(!empty($host)) { - fputs($fp, "Host: $host\r\n"); - } - if(!empty($accept)) { - fputs($fp, "Accept: $accept\r\n"); - } - if(!empty($agent)) { - fputs($fp, "User-Agent: $agent\r\n"); - } - if(!empty($referer)) { - fputs($fp, "Referer: $referer\r\n"); - } - if(isset($extra_headers) && is_array($extra_headers)) { - foreach($extra_headers as $curr_header) { - fputs($fp, $curr_header."\r\n"); - } - } - if(!empty($user) && !empty($pass)) { - fputs($fp, "Authorization: BASIC ".base64_encode("$user:$pass")."\r\n"); - } - - fputs($fp, "\r\n"); - while(!feof($fp)) { - $content .= fgets($fp,4096); - } - fclose($fp); - $csplit = preg_split("!\r\n\r\n!",$content,2); - - $content = $csplit[1]; - - if(!empty($params['assign_headers'])) { - $template->assign($params['assign_headers'],preg_split("!\r\n!",$csplit[0])); - } - } - } else { - trigger_error("[plugin fetch] unable to parse URL, check syntax",E_USER_NOTICE); - return; - } - } else { - // ftp fetch - if($fp = @fopen($params['file'],'r')) { - while(!feof($fp)) { - $content .= fgets ($fp,4096); - } - fclose($fp); - } else { - trigger_error('[plugin] fetch cannot read file \'' . $params['file'] .'\'',E_USER_NOTICE); - return; - } + $content = @file_get_contents($params['file']); + if ($content === false) { + throw new SmartyException("{fetch} cannot read resource '" . $params['file'] ."'"); } - } - if (!empty($params['assign'])) { - $template->assign($params['assign'],$content); + $template->assign($params['assign'], $content); } else { return $content; } diff --git a/libs/plugins/function.html_image.php b/libs/plugins/function.html_image.php index 2f1ef641..6521966b 100644 --- a/libs/plugins/function.html_image.php +++ b/libs/plugins/function.html_image.php @@ -46,8 +46,7 @@ function smarty_function_html_image($params, $template) $prefix = ''; $suffix = ''; $path_prefix = ''; - $server_vars = $_SERVER; - $basedir = isset($server_vars['DOCUMENT_ROOT']) ? $server_vars['DOCUMENT_ROOT'] : ''; + $basedir = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; foreach($params as $_key => $_val) { switch ($_key) { case 'file': @@ -88,13 +87,38 @@ function smarty_function_html_image($params, $template) return; } - if (substr($file, 0, 1) == '/') { + if ($file[0] == '/') { $_image_path = $basedir . $file; } else { $_image_path = $file; - } + } + + // strip file protocol + if (stripos($params['file'], 'file://') === 0) { + $params['file'] = substr($params['file'], 7); + } + + $protocol = strpos($params['file'], '://'); + if ($protocol !== false) { + $protocol = strtolower(substr($params['file'], 0, $protocol)); + } + + if (isset($template->smarty->security_policy)) { + if ($protocol) { + // remote resource (or php stream, …) + if(!$template->smarty->security_policy->isTrustedUri($params['file'])) { + return; + } + } else { + // local file + if(!$template->smarty->security_policy->isTrustedResourceDir($params['file'])) { + return; + } + } + } if (!isset($params['width']) || !isset($params['height'])) { + // FIXME: (rodneyrehm) getimagesize() loads the complete file off a remote resource, use custom [jpg,png,gif]header reader! if (!$_image_data = @getimagesize($_image_path)) { if (!file_exists($_image_path)) { trigger_error("html_image: unable to find '$_image_path'", E_USER_NOTICE); @@ -106,12 +130,7 @@ function smarty_function_html_image($params, $template) trigger_error("html_image: '$_image_path' is not a valid image file", E_USER_NOTICE); return; } - } - if (isset($template->smarty->security_policy)) { - if (!$template->smarty->security_policy->isTrustedResourceDir($_image_path)) { - return; - } - } + } if (!isset($params['width'])) { $width = $_image_data[0]; @@ -122,7 +141,9 @@ function smarty_function_html_image($params, $template) } if (isset($params['dpi'])) { - if (strstr($server_vars['HTTP_USER_AGENT'], 'Mac')) { + if (strstr($_SERVER['HTTP_USER_AGENT'], 'Mac')) { + // FIXME: (rodneyrehm) wrong dpi assumption + // don't know who thought this up… even if it was true in 1998, it's definitely wrong in 2011. $dpi_default = 72; } else { $dpi_default = 96; diff --git a/libs/sysplugins/smarty_security.php b/libs/sysplugins/smarty_security.php index 3d4f3189..90b0e3e2 100644 --- a/libs/sysplugins/smarty_security.php +++ b/libs/sysplugins/smarty_security.php @@ -48,6 +48,12 @@ class Smarty_Security { * @var array */ public $trusted_dir = array(); + /** + * List of regular expressions (PCRE) that include trusted URIs + * + * @var array + */ + public $trusted_uri = array(); /** * This is an array of trusted static classes. * @@ -374,7 +380,33 @@ class Smarty_Security { // give up throw new SmartyException("directory '{$_filepath}' not allowed by security setting"); } - + + /** + * Check if URI (e.g. {fetch} or {html_image}) is trusted + * + * To simplify things, isTrustedUri() resolves all input to "{$PROTOCOL}://{$HOSTNAME}". + * So "http://username:password@hello.world.example.org:8080/some-path?some=query-string" + * is reduced to "http://hello.world.example.org" prior to applying the patters from {@link $trusted_uri}. + * @param string $uri + * @return boolean true if URI is trusted + * @throws SmartyException if URI is not trusted + * @uses $trusted_uri for list of patterns to match against $uri + */ + public function isTrustedUri($uri) + { + $_uri = parse_url($uri); + if (!empty($_uri['scheme']) && !empty($_uri['host'])) { + $_uri = $_uri['scheme'] . '://' . $_uri['host']; + foreach ($this->trusted_uri as $pattern) { + if (preg_match($pattern, $_uri)) { + return true; + } + } + } + + throw new SmartyException("URI '{$uri}' not allowed by security setting"); + } + /** * Check if directory of file resource is trusted. *