diff --git a/bootstrap.php b/bootstrap.php index e70c79a6bb9801b06c49e4aaa5349a93e84b6de6..fe3dd2155b8fdd0e2dbb21d249d5eeba247fadc0 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -50,11 +50,7 @@ class Bootstrap { } function https() { - return - (isset($_SERVER['HTTPS']) - && strtolower($_SERVER['HTTPS']) == 'on') - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) - && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'); + return osTicket::is_https(); } static function defineTables($prefix) { @@ -335,6 +331,7 @@ ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR) require(INCLUDE_DIR.'class.osticket.php'); require(INCLUDE_DIR.'class.misc.php'); require(INCLUDE_DIR.'class.http.php'); +require(INCLUDE_DIR.'class.validator.php'); // Determine the path in the URI used as the base of the osTicket // installation @@ -346,12 +343,7 @@ Bootstrap::init(); #CURRENT EXECUTING SCRIPT. define('THISPAGE', Misc::currentURL()); -define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5); -define('DEFAULT_PRIORITY_ID',1); +define('DEFAULT_MAX_FILE_UPLOADS', ini_get('max_file_uploads') ?: 5); +define('DEFAULT_PRIORITY_ID', 1); -#Global override -if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) - // Take the left-most item for X-Forwarded-For - $_SERVER['REMOTE_ADDR'] = trim(array_pop( - explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']))); ?> diff --git a/include/class.osticket.php b/include/class.osticket.php index 40c939c0493a30d685ea7c51fac0d808eb5b39a5..db48388e03dc852c9a9ddc06ffe9c9bf7a030211 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -444,6 +444,37 @@ class osTicket { } } + /* + * getTrustedProxies + * + * Get defined trusted proxies + */ + + static function getTrustedProxies() { + static $proxies = null; + // Parse trusted proxies from config file + if (!isset($proxies) && defined('TRUSTED_PROXIES')) + $proxies = array_filter( + array_map('trim', explode(',', TRUSTED_PROXIES))); + + return $proxies ?: array(); + } + + /* + * getLocalNetworkAddresses + * + * Get defined local network addresses + */ + static function getLocalNetworkAddresses() { + static $ips = null; + // Parse local addreses from config file + if (!isset($ips) && defined('LOCAL_NETWORKS')) + $ips = array_filter( + array_map('trim', explode(',', LOCAL_NETWORKS))); + + return $ips ?: array(); + } + static function get_root_path($dir) { /* If run from the commandline, DOCUMENT_ROOT will not be set. It is @@ -488,14 +519,96 @@ class osTicket { return null; } + /* + * get_client_ip + * + * Get client IP address from "Http_X-Forwarded-For" header by following a + * chain of trusted proxies. + * + * "Http_X-Forwarded-For" header value is a comma+space separated list of IP + * addresses, the left-most being the original client, and each successive + * proxy that passed the request all the way to the originating IP address. + * + */ + static function get_client_ip($header='HTTP_X_FORWARDED_FOR') { + + // Request IP + $ip = $_SERVER['REMOTE_ADDR']; + // Trusted proxies. + $proxies = self::getTrustedProxies(); + // Return current IP address if header is not set and + // request is not from a trusted proxy. + if (!isset($_SERVER[$header]) + || !$proxies + || !self::is_trusted_proxy($ip, $proxies)) + return $ip; + + // Get chain of proxied ip addresses + $ips = array_map('trim', explode(',', $_SERVER[$header])); + // Add request IP to the chain + $ips[] = $ip; + // Walk the chain in reverse - remove invalid IPs + $ips = array_reverse($ips); + foreach ($ips as $k => $ip) { + // Make sure the IP is valid and not a trusted proxy + if ($k && !Validator::is_ip($ip)) + unset($ips[$k]); + elseif ($k && !self::is_trusted_proxy($ip, $proxies)) + return $ip; + } + + // We trust the 400 lb hacker... return left most valid IP + return array_pop($ips); + } + + /* + * Checks if the IP is that of a trusted proxy + * + */ + static function is_trusted_proxy($ip, $proxies=array()) { + $proxies = $proxies ?: self::getTrustedProxies(); + // We don't have any proxies set. + if (!$proxies) + return false; + // Wildcard set - trust all proxies + else if ($proxies == '*') + return true; + + return ($proxies && Validator::check_ip($ip, $proxies)); + } + + /** + * is_local_ip + * + * Check if a given IP is part of defined local address blocks + * + */ + static function is_local_ip($ip, $ips=array()) { + $ips = $ips + ?: self::getLocalNetworkAddresses() + ?: array(); + + foreach ($ips as $addr) { + if (Validator::check_ip($ip, $addr)) + return true; + } + + return false; + } + /** * Returns TRUE if the request was made via HTTPS and false otherwise */ function is_https() { - return (isset($_SERVER['HTTPS']) + + // Local server flags + if (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) - && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'); + return true; + + // Check if SSL was terminated by a loadbalancer + return (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) + && !strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https')); } /* returns true if script is being executed via commandline */ diff --git a/include/class.validator.php b/include/class.validator.php index 2cce38f21432dc457efbae59bd4277bec9d213d3..14be7ccaf005423bf34c395aa375c04227e9671e 100644 --- a/include/class.validator.php +++ b/include/class.validator.php @@ -185,23 +185,7 @@ class Validator { } static function is_ip($ip) { - - if(!$ip or empty($ip)) - return false; - - $ip=trim($ip); - # Thanks to http://stackoverflow.com/a/1934546 - if (function_exists('inet_pton')) { # PHP 5.1.0 - # Let the built-in library parse the IP address - return @inet_pton($ip) !== false; - } else if (preg_match( - '/^(?>(?>([a-f0-9]{1,4})(?>:(?1)){7}|(?!(?:.*[a-f0-9](?>:|$)){7,})' - .'((?1)(?>:(?1)){0,5})?::(?2)?)|(?>(?>(?1)(?>:(?1)){5}:|(?!(?:.*[a-f0-9]:){5,})' - .'(?3)?::(?>((?1)(?>:(?1)){0,3}):)?)?(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])' - .'(?>\.(?4)){3}))$/iD', $ip)) { - return true; - } - return false; + return filter_var(trim($ip), FILTER_VALIDATE_IP) !== false; } static function is_username($username, &$error='') { @@ -212,6 +196,100 @@ class Validator { return $error == ''; } + + /* + * check_ip + * Checks if an IP (IPv4 or IPv6) address is contained in the list of given IPs or subnets. + * + * @credit - borrowed from Symfony project + * + */ + public static function check_ip($ip, $ips) { + + if (!Validator::is_ip($ip)) + return false; + + $method = substr_count($ip, ':') > 1 ? 'check_ipv6' : 'check_ipv4'; + $ips = is_array($ips) ? $ips : array($ips); + foreach ($ips as $_ip) { + if (self::$method($ip, $_ip)) { + return true; + } + } + + return false; + } + + /** + * check_ipv4 + * Compares two IPv4 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @credit - borrowed from Symfony project + */ + public static function check_ipv4($ip, $cidr) { + + if (false !== strpos($cidr, '/')) { + list($address, $netmask) = explode('/', $cidr, 2); + + if ($netmask === '0') + return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + + if ($netmask < 0 || $netmask > 32) + return false; + + } else { + $address = $cidr; + $netmask = 32; + } + + return 0 === substr_compare( + sprintf('%032b', ip2long($ip)), + sprintf('%032b', ip2long($address)), + 0, $netmask); + } + + /** + * Compares two IPv6 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @credit - borrowed from Symfony project + * @author David Soria Parra <dsp at php dot net> + * + * @see https://github.com/dsp/v6tools + * + */ + public static function check_ipv6($ip, $cidr) { + + if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1'))) + return false; + + if (false !== strpos($cidr, '/')) { + list($address, $netmask) = explode('/', $cidr, 2); + if ($netmask < 1 || $netmask > 128) + return false; + } else { + $address = $cidr; + $netmask = 128; + } + + $bytesAddr = unpack('n*', @inet_pton($address)); + $bytesTest = unpack('n*', @inet_pton($ip)); + if (!$bytesAddr || !$bytesTest) + return false; + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xffff >> $left) & 0xffff; + if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { + return false; + } + } + + return true; + } + function process($fields,$vars,&$errors){ $val = new Validator(); diff --git a/include/ost-sampleconfig.php b/include/ost-sampleconfig.php index 0b26400698271c1d89ab46f55a209d501b5b1dae..b4a5049f4d01b5a9596c0142211f41bd19e73cbd 100644 --- a/include/ost-sampleconfig.php +++ b/include/ost-sampleconfig.php @@ -107,6 +107,39 @@ define('TABLE_PREFIX','%CONFIG-PREFIX'); # define('ROOT_PATH', '/support/'); + +# Option: TRUSTED_PROXIES (default: <none>) +# +# To support running osTicket installation on a web servers that sit behind a +# load balancer, HTTP cache, or other intermediary (reverse) proxy; it's +# necessary to define trusted proxies to protect against forged http headers +# +# osTicket supports passing the following http headers from a trusted proxy; +# - HTTP_X_FORWARDED_FOR => Chain of client's IPs +# - HTTP_X_FORWARDED_PROTO => Client's HTTP protocal (http | https) +# +# You'll have to explicitly define comma separated IP addreseses or CIDR of +# upstream proxies to trust. Wildcard "*" (not recommended) can be used to +# trust all chained IPs as proxies in cases that ISP/host doesn't provide +# IPs of loadbalancers or proxies. +# +# References: +# http://en.wikipedia.org/wiki/X-Forwarded-For +# + +define('TRUSTED_PROXIES', ''); + + +# Option: LOCAL_NETWORKS (default: 127.0.0.0/24) +# +# When running osTicket as part of a cluster it might become necessary to +# whitelist local/virtual networks that can bypass some authentication/checks. +# +# define comma separated IP addreseses or enter CIDR of local network. + +define('LOCAL_NETWORKS', '127.0.0.0/24'); + + # # Session Storage Options # --------------------------------------------------- diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php index 0d3f05e216a6e898f93b2a1134f497a2746ee6cf..9d4d43df1dbd020a61be1defc16109591bff1db3 100644 --- a/include/staff/syslogs.inc.php +++ b/include/staff/syslogs.inc.php @@ -156,7 +156,7 @@ else <td> <a class="tip" href="#log/<?php echo $row['log_id']; ?>"><?php echo Format::htmlchars($row['title']); ?></a></td> <td><?php echo $row['log_type']; ?></td> <td> <?php echo Format::daydatetime($row['created']); ?></td> - <td><?php echo $row['ip_address']; ?></td> + <td><?php echo Format::htmlchars($row['ip_address']); ?></td> </tr> <?php } //end of while. diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 0ebf5cbf2e134d6d557556c053b848da35e516c6..cc93efcc3f97091ecaeafe51e3679ce1ea252e8c 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -355,7 +355,7 @@ if($ticket->isOverdue()) echo Format::htmlchars($ticket->getSource()); if (!strcasecmp($ticket->getSource(), 'Web') && $ticket->getIP()) - echo ' <span class="faded">('.$ticket->getIP().')</span>'; + echo ' <span class="faded">('.Format::htmlchars($ticket->getIP()).')</span>'; ?> </td> </tr> diff --git a/main.inc.php b/main.inc.php index 026e440ca84cc904d8a5ebcf8e6b5101cd81d41e..e92ec4a71d3e59876361fba0aa8446328b383779 100644 --- a/main.inc.php +++ b/main.inc.php @@ -27,6 +27,9 @@ Bootstrap::i18n_prep(); Bootstrap::loadCode(); Bootstrap::connect(); +#Global override +$_SERVER['REMOTE_ADDR'] = osTicket::get_client_ip(); + if(!($ost=osTicket::start()) || !($cfg = $ost->getConfig())) Bootstrap::croak(__('Unable to load config info from DB. Get tech support.')); diff --git a/setup/test/tests/test.validation.php b/setup/test/tests/test.validation.php index bce9fe85505d08e41251fc127e230bba9f775394..0297d51d7d61535f82e27548af169aa7222bb46d 100644 --- a/setup/test/tests/test.validation.php +++ b/setup/test/tests/test.validation.php @@ -54,6 +54,19 @@ class TestValidation extends Test { #$this->assert(Validator::is_email('δοκιμή@παÏάδειγμα.δοκιμή')); #$this->assert(Validator::is_email('甲æ–@é»’å·.日本')); } + + function testIPAddresses() { + + // Validate IP Addreses + $this->assert(Validator::is_ip('127.0.0.1')); + $this->assert(Validator::is_ip('192.168.129.74')); + + // Test IP check + $this->assert(Validator::check_ip('127.0.0.1', '127.0.0.0/24')); + $this->assert(Validator::check_ip('192.168.129.42', + ['127.0.0.0/24', '192.168.129.0/24'])); + $this->assert(!Validator::check_ip('10.0.5.15', '127.0.0.0/24')); + } } return 'TestValidation'; ?>