Skip to content

Commit 4396f91

Browse files
committed
Add Trusted Proxies and LAN Options
* HTTP 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. * HTTP Option: LOCAL_NETWORKS (default: 127.0.0.0/24) When running osTicket as part of a cluster it might become necessary to white list local/virtual networks that can bypass some authentication checks. * Validate CLIENT_IP to make sure it's a valid IP address.
1 parent 2fb47bd commit 4396f91

8 files changed

Lines changed: 266 additions & 34 deletions

File tree

bootstrap.php

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,7 @@ static function init() {
5050
}
5151

5252
function https() {
53-
return
54-
(isset($_SERVER['HTTPS'])
55-
&& strtolower($_SERVER['HTTPS']) == 'on')
56-
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
57-
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https');
53+
return osTicket::is_https();
5854
}
5955

6056
static function defineTables($prefix) {
@@ -335,6 +331,7 @@ function croak($message) {
335331
require(INCLUDE_DIR.'class.osticket.php');
336332
require(INCLUDE_DIR.'class.misc.php');
337333
require(INCLUDE_DIR.'class.http.php');
334+
require(INCLUDE_DIR.'class.validator.php');
338335

339336
// Determine the path in the URI used as the base of the osTicket
340337
// installation
@@ -346,12 +343,7 @@ function croak($message) {
346343
#CURRENT EXECUTING SCRIPT.
347344
define('THISPAGE', Misc::currentURL());
348345

349-
define('DEFAULT_MAX_FILE_UPLOADS',ini_get('max_file_uploads')?ini_get('max_file_uploads'):5);
350-
define('DEFAULT_PRIORITY_ID',1);
346+
define('DEFAULT_MAX_FILE_UPLOADS', ini_get('max_file_uploads') ?: 5);
347+
define('DEFAULT_PRIORITY_ID', 1);
351348

352-
#Global override
353-
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
354-
// Take the left-most item for X-Forwarded-For
355-
$_SERVER['REMOTE_ADDR'] = trim(array_pop(
356-
explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])));
357349
?>

include/class.osticket.php

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,37 @@ function getLatestVersion($product='core', $major=null) {
444444
}
445445
}
446446

447+
/*
448+
* getTrustedProxies
449+
*
450+
* Get defined trusted proxies
451+
*/
452+
453+
static function getTrustedProxies() {
454+
static $proxies = null;
455+
// Parse trusted proxies from config file
456+
if (!isset($proxies) && defined('TRUSTED_PROXIES'))
457+
$proxies = array_filter(
458+
array_map('trim', explode(',', TRUSTED_PROXIES)));
459+
460+
return $proxies ?: array();
461+
}
462+
463+
/*
464+
* getLocalNetworkAddresses
465+
*
466+
* Get defined local network addresses
467+
*/
468+
static function getLocalNetworkAddresses() {
469+
static $ips = null;
470+
// Parse local addreses from config file
471+
if (!isset($ips) && defined('LOCAL_NETWORKS'))
472+
$ips = array_filter(
473+
array_map('trim', explode(',', LOCAL_NETWORKS)));
474+
475+
return $ips ?: array();
476+
}
477+
447478
static function get_root_path($dir) {
448479

449480
/* If run from the commandline, DOCUMENT_ROOT will not be set. It is
@@ -488,14 +519,96 @@ static function get_root_path($dir) {
488519
return null;
489520
}
490521

522+
/*
523+
* get_client_ip
524+
*
525+
* Get client IP address from "Http_X-Forwarded-For" header by following a
526+
* chain of trusted proxies.
527+
*
528+
* "Http_X-Forwarded-For" header value is a comma+space separated list of IP
529+
* addresses, the left-most being the original client, and each successive
530+
* proxy that passed the request all the way to the originating IP address.
531+
*
532+
*/
533+
static function get_client_ip($header='HTTP_X_FORWARDED_FOR') {
534+
535+
// Request IP
536+
$ip = $_SERVER['REMOTE_ADDR'];
537+
// Trusted proxies.
538+
$proxies = self::getTrustedProxies();
539+
// Return current IP address if header is not set and
540+
// request is not from a trusted proxy.
541+
if (!isset($_SERVER[$header])
542+
|| !$proxies
543+
|| !self::is_trusted_proxy($ip, $proxies))
544+
return $ip;
545+
546+
// Get chain of proxied ip addresses
547+
$ips = array_map('trim', explode(',', $_SERVER[$header]));
548+
// Add request IP to the chain
549+
$ips[] = $ip;
550+
// Walk the chain in reverse - remove invalid IPs
551+
$ips = array_reverse($ips);
552+
foreach ($ips as $k => $ip) {
553+
// Make sure the IP is valid and not a trusted proxy
554+
if ($k && !Validator::is_ip($ip))
555+
unset($ips[$k]);
556+
elseif ($k && !self::is_trusted_proxy($ip, $proxies))
557+
return $ip;
558+
}
559+
560+
// We trust the 400 lb hacker... return left most valid IP
561+
return array_pop($ips);
562+
}
563+
564+
/*
565+
* Checks if the IP is that of a trusted proxy
566+
*
567+
*/
568+
static function is_trusted_proxy($ip, $proxies=array()) {
569+
$proxies = $proxies ?: self::getTrustedProxies();
570+
// We don't have any proxies set.
571+
if (!$proxies)
572+
return false;
573+
// Wildcard set - trust all proxies
574+
else if ($proxies == '*')
575+
return true;
576+
577+
return ($proxies && Validator::check_ip($ip, $proxies));
578+
}
579+
580+
/**
581+
* is_local_ip
582+
*
583+
* Check if a given IP is part of defined local address blocks
584+
*
585+
*/
586+
static function is_local_ip($ip, $ips=array()) {
587+
$ips = $ips
588+
?: self::getLocalNetworkAddresses()
589+
?: array();
590+
591+
foreach ($ips as $addr) {
592+
if (Validator::check_ip($ip, $addr))
593+
return true;
594+
}
595+
596+
return false;
597+
}
598+
491599
/**
492600
* Returns TRUE if the request was made via HTTPS and false otherwise
493601
*/
494602
function is_https() {
495-
return (isset($_SERVER['HTTPS'])
603+
604+
// Local server flags
605+
if (isset($_SERVER['HTTPS'])
496606
&& strtolower($_SERVER['HTTPS']) == 'on')
497-
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
498-
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https');
607+
return true;
608+
609+
// Check if SSL was terminated by a loadbalancer
610+
return (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
611+
&& !strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https'));
499612
}
500613

501614
/* returns true if script is being executed via commandline */

include/class.validator.php

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -185,23 +185,7 @@ static function is_url($url) {
185185
}
186186

187187
static function is_ip($ip) {
188-
189-
if(!$ip or empty($ip))
190-
return false;
191-
192-
$ip=trim($ip);
193-
# Thanks to http://stackoverflow.com/a/1934546
194-
if (function_exists('inet_pton')) { # PHP 5.1.0
195-
# Let the built-in library parse the IP address
196-
return @inet_pton($ip) !== false;
197-
} else if (preg_match(
198-
'/^(?>(?>([a-f0-9]{1,4})(?>:(?1)){7}|(?!(?:.*[a-f0-9](?>:|$)){7,})'
199-
.'((?1)(?>:(?1)){0,5})?::(?2)?)|(?>(?>(?1)(?>:(?1)){5}:|(?!(?:.*[a-f0-9]:){5,})'
200-
.'(?3)?::(?>((?1)(?>:(?1)){0,3}):)?)?(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])'
201-
.'(?>\.(?4)){3}))$/iD', $ip)) {
202-
return true;
203-
}
204-
return false;
188+
return filter_var(trim($ip), FILTER_VALIDATE_IP) !== false;
205189
}
206190

207191
static function is_username($username, &$error='') {
@@ -212,6 +196,100 @@ static function is_username($username, &$error='') {
212196
return $error == '';
213197
}
214198

199+
200+
/*
201+
* check_ip
202+
* Checks if an IP (IPv4 or IPv6) address is contained in the list of given IPs or subnets.
203+
*
204+
* @credit - borrowed from Symfony project
205+
*
206+
*/
207+
public static function check_ip($ip, $ips) {
208+
209+
if (!Validator::is_ip($ip))
210+
return false;
211+
212+
$method = substr_count($ip, ':') > 1 ? 'check_ipv6' : 'check_ipv4';
213+
$ips = is_array($ips) ? $ips : array($ips);
214+
foreach ($ips as $_ip) {
215+
if (self::$method($ip, $_ip)) {
216+
return true;
217+
}
218+
}
219+
220+
return false;
221+
}
222+
223+
/**
224+
* check_ipv4
225+
* Compares two IPv4 addresses.
226+
* In case a subnet is given, it checks if it contains the request IP.
227+
*
228+
* @credit - borrowed from Symfony project
229+
*/
230+
public static function check_ipv4($ip, $cidr) {
231+
232+
if (false !== strpos($cidr, '/')) {
233+
list($address, $netmask) = explode('/', $cidr, 2);
234+
235+
if ($netmask === '0')
236+
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
237+
238+
if ($netmask < 0 || $netmask > 32)
239+
return false;
240+
241+
} else {
242+
$address = $cidr;
243+
$netmask = 32;
244+
}
245+
246+
return 0 === substr_compare(
247+
sprintf('%032b', ip2long($ip)),
248+
sprintf('%032b', ip2long($address)),
249+
0, $netmask);
250+
}
251+
252+
/**
253+
* Compares two IPv6 addresses.
254+
* In case a subnet is given, it checks if it contains the request IP.
255+
*
256+
* @credit - borrowed from Symfony project
257+
* @author David Soria Parra <dsp at php dot net>
258+
*
259+
* @see https://github.com/dsp/v6tools
260+
*
261+
*/
262+
public static function check_ipv6($ip, $cidr) {
263+
264+
if (!((extension_loaded('sockets') && defined('AF_INET6')) || @inet_pton('::1')))
265+
return false;
266+
267+
if (false !== strpos($cidr, '/')) {
268+
list($address, $netmask) = explode('/', $cidr, 2);
269+
if ($netmask < 1 || $netmask > 128)
270+
return false;
271+
} else {
272+
$address = $cidr;
273+
$netmask = 128;
274+
}
275+
276+
$bytesAddr = unpack('n*', @inet_pton($address));
277+
$bytesTest = unpack('n*', @inet_pton($ip));
278+
if (!$bytesAddr || !$bytesTest)
279+
return false;
280+
281+
for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
282+
$left = $netmask - 16 * ($i - 1);
283+
$left = ($left <= 16) ? $left : 16;
284+
$mask = ~(0xffff >> $left) & 0xffff;
285+
if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
286+
return false;
287+
}
288+
}
289+
290+
return true;
291+
}
292+
215293
function process($fields,$vars,&$errors){
216294

217295
$val = new Validator();

include/ost-sampleconfig.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,39 @@
107107

108108
# define('ROOT_PATH', '/support/');
109109

110+
111+
# Option: TRUSTED_PROXIES (default: <none>)
112+
#
113+
# To support running osTicket installation on a web servers that sit behind a
114+
# load balancer, HTTP cache, or other intermediary (reverse) proxy; it's
115+
# necessary to define trusted proxies to protect against forged http headers
116+
#
117+
# osTicket supports passing the following http headers from a trusted proxy;
118+
# - HTTP_X_FORWARDED_FOR => Chain of client's IPs
119+
# - HTTP_X_FORWARDED_PROTO => Client's HTTP protocal (http | https)
120+
#
121+
# You'll have to explicitly define comma separated IP addreseses or CIDR of
122+
# upstream proxies to trust. Wildcard "*" (not recommended) can be used to
123+
# trust all chained IPs as proxies in cases that ISP/host doesn't provide
124+
# IPs of loadbalancers or proxies.
125+
#
126+
# References:
127+
# http://en.wikipedia.org/wiki/X-Forwarded-For
128+
#
129+
130+
define('TRUSTED_PROXIES', '');
131+
132+
133+
# Option: LOCAL_NETWORKS (default: 127.0.0.0/24)
134+
#
135+
# When running osTicket as part of a cluster it might become necessary to
136+
# whitelist local/virtual networks that can bypass some authentication/checks.
137+
#
138+
# define comma separated IP addreseses or enter CIDR of local network.
139+
140+
define('LOCAL_NETWORKS', '127.0.0.0/24');
141+
142+
110143
#
111144
# Session Storage Options
112145
# ---------------------------------------------------

include/staff/syslogs.inc.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
<td>&nbsp;<a class="tip" href="#log/<?php echo $row['log_id']; ?>"><?php echo Format::htmlchars($row['title']); ?></a></td>
157157
<td><?php echo $row['log_type']; ?></td>
158158
<td>&nbsp;<?php echo Format::daydatetime($row['created']); ?></td>
159-
<td><?php echo $row['ip_address']; ?></td>
159+
<td><?php echo Format::htmlchars($row['ip_address']); ?></td>
160160
</tr>
161161
<?php
162162
} //end of while.

include/staff/ticket-view.inc.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ class="icon-building icon-fixed-width"></i> <?php
355355
echo Format::htmlchars($ticket->getSource());
356356

357357
if (!strcasecmp($ticket->getSource(), 'Web') && $ticket->getIP())
358-
echo '&nbsp;&nbsp; <span class="faded">('.$ticket->getIP().')</span>';
358+
echo '&nbsp;&nbsp; <span class="faded">('.Format::htmlchars($ticket->getIP()).')</span>';
359359
?>
360360
</td>
361361
</tr>

main.inc.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
Bootstrap::loadCode();
2828
Bootstrap::connect();
2929

30+
#Global override
31+
$_SERVER['REMOTE_ADDR'] = osTicket::get_client_ip();
32+
3033
if(!($ost=osTicket::start()) || !($cfg = $ost->getConfig()))
3134
Bootstrap::croak(__('Unable to load config info from DB. Get tech support.'));
3235

setup/test/tests/test.validation.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ function testValidEmail() {
5454
#$this->assert(Validator::is_email('δοκιμή@παράδειγμα.δοκιμή'));
5555
#$this->assert(Validator::is_email('甲斐@黒川.日本'));
5656
}
57+
58+
function testIPAddresses() {
59+
60+
// Validate IP Addreses
61+
$this->assert(Validator::is_ip('127.0.0.1'));
62+
$this->assert(Validator::is_ip('192.168.129.74'));
63+
64+
// Test IP check
65+
$this->assert(Validator::check_ip('127.0.0.1', '127.0.0.0/24'));
66+
$this->assert(Validator::check_ip('192.168.129.42',
67+
['127.0.0.0/24', '192.168.129.0/24']));
68+
$this->assert(!Validator::check_ip('10.0.5.15', '127.0.0.0/24'));
69+
}
5770
}
5871
return 'TestValidation';
5972
?>

0 commit comments

Comments
 (0)