[TikiWiki-commits] [Git][tikiwiki/tiki][24.x] [BP][ENH] Load Balancer: Add preference to allow access from trusted IPs only
"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <698385b216fd_3b1844a8915f8@gitlab-sidekiq-low-urgency-cpu-bound-v2-55cfcc57bd-lrq62.mail> |
Elifeleti Mukisa Dan pushed to branch 24.x at Tiki Wiki CMS Groupware / Tiki Commits: 1bb1f4f6 by Elifeleti Mukisa Dan at 2026-02-04T17:38:31+00:00 [BP][ENH] Load Balancer: Add preference to allow access from trusted IPs only --- * [BP][ENH] Load Balancer: Add preference to allow access from trusted IPs only --- * [BP][ENH] Load Balancer: Add preference to allow access from trusted IPs only --- * [ENH] Load Balancer: Add preference to allow access from trusted IPs only --- * [ENH] Load Balancer: Add preference to allow access from trusted IPs only --- * [ENH] Load Balancer: Add preference to allow access from trusted IPs only (cherry picked from commit e11fe7d02a4ff5b5ea4329feea411b856f7c8453) ae0d44be [FIX] Add preference to allow trusted Ips Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!9484 (cherry picked from commit c9052302e524d1965bc90bd0af42033ad64ab29c) c056882e [ENH] Load Balancer: Add preference to allow access from trusted IPs only Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!9485 See merge request tikiwiki/tiki!9491 See merge request tikiwiki/tiki!9502 See merge request tikiwiki/tiki!9507 - - - - - 4 changed files: - lib/prefs/feature.php - lib/tikilib.php - lib/validators/validator_uniqueemail.php - tiki-monitor.php Changes: ===================================== lib/prefs/feature.php ===================================== @@ -2706,6 +2706,23 @@ function prefs_feature_list($partial = false) 'default' => 'n', 'tags' => ['experimental'], ], + 'feature_loadbalancer_trusted_proxies' => [ + 'name' => tra('Trusted reverse proxy IPs'), + 'description' => tra('List of IP addresses of trusted reverse proxies. Only requests coming from these IPs will have their X-Forwarded-For headers trusted. Leave empty to trust all IPs (less secure).'), + 'type' => 'textarea', + 'size' => 3, + 'default' => '', + 'tags' => ['experimental'], + 'dependencies' => ['feature_loadbalancer'], + ], + 'feature_loadbalancer_header' => [ + 'name' => tra('Reverse proxy header'), + 'description' => tra('Name of the HTTP header used by your reverse proxy to pass the client IP. Common values: X-Forwarded-For, CF-Connecting-IP, X-Real-IP, X-Client-IP. There is no auto-detection.'), + 'type' => 'text', + 'default' => '', + 'tags' => ['experimental'], + 'dependencies' => ['feature_loadbalancer'], + ], 'feature_port_rewriting' => [ 'name' => tra('Tiki is behind a frontend-proxy/load-balancer that rewrites ports'), 'description' => tra('Activate this only if the server is behind a frontend-proxy/load-balancer (or reverse proxy) that rewrites ports. This enables Tiki to use the HTTP_X_FORWARDED_PROTO parameter set by the proxy, to provide correct links.'), ===================================== lib/tikilib.php ===================================== @@ -553,6 +553,7 @@ class TikiLib extends TikiDb_Bridge /*shared*/ // Returns IP address or IP address forwarded by the proxy if feature load balancer is set + // Security: Only trusts proxy headers from trusted reverse proxy IPs /** * @param $firewall true to detect ip behind a firewall * @return null|string @@ -560,35 +561,21 @@ class TikiLib extends TikiDb_Bridge public function get_ip_address($firewall = 0) { global $prefs; - if ($firewall || (isset($prefs['feature_loadbalancer']) && $prefs['feature_loadbalancer'] === "y")) { - $header_checks = [ - 'HTTP_CF_CONNECTING_IP', - 'HTTP_CLIENT_IP', - 'HTTP_PRAGMA', - 'HTTP_XONNECTION', - 'HTTP_CACHE_INFO', - 'HTTP_XPROXY', - 'HTTP_PROXY', - 'HTTP_PROXY_RENAMED', - 'HTTP_PROXY_CONNECTION', - 'HTTP_VIA', - 'HTTP_X_COMING_FROM', - 'HTTP_COMING_FROM', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_FORWARDED', - 'HTTP_X_CLUSTER_CLIENT_IP', - 'HTTP_FORWARDED_FOR', - 'HTTP_FORWARDED', - 'HTTP_CACHE_CONTROL', - 'HTTP_X_REAL_IP', - 'REMOTE_ADDR']; - - foreach ($header_checks as $key) { - if (array_key_exists($key, $_SERVER) === true) { - foreach (explode(',', $_SERVER[$key]) as $ip) { - $ip = trim($ip); - //filter the ip with filter functions + // Check if we should trust proxy headers + $should_check_proxy_headers = $firewall || (isset($prefs['feature_loadbalancer']) && $prefs['feature_loadbalancer'] === "y"); + + if ($should_check_proxy_headers) { + // Security: Verify that the request comes from a trusted proxy + $isFromTrustedProxy = $this->isFromTrustedProxy(); + + if ($isFromTrustedProxy) { + // Determine which header to check + $header_to_check = $this->getProxyHeaderName(); + + if (! empty($header_to_check) && array_key_exists($header_to_check, $_SERVER)) { + foreach (explode(',', $_SERVER[$header_to_check]) as $ip) { + $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP) !== false) { return $ip; } @@ -596,6 +583,8 @@ class TikiLib extends TikiDb_Bridge } } } + + // Fall back to REMOTE_ADDR if proxy headers are not available or not trusted if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) { return $_SERVER['REMOTE_ADDR']; } else { @@ -603,6 +592,57 @@ class TikiLib extends TikiDb_Bridge } } + /*shared*/ + /** + * Check if the current request comes from a trusted reverse proxy + * @return bool + */ + public function isFromTrustedProxy() + { + global $prefs; + + // If feature_loadbalancer is not enabled, don't trust proxy headers + if (! isset($prefs['feature_loadbalancer']) || $prefs['feature_loadbalancer'] !== "y") { + return false; + } + + $trusted_proxies = isset($prefs['feature_loadbalancer_trusted_proxies']) ? $prefs['feature_loadbalancer_trusted_proxies'] : ''; + $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + + // If no trusted proxies are configured, don't trust any proxy headers (most secure) + if (empty($trusted_proxies)) { + return false; + } + + // Parse the trusted proxies list + $trusted_ips = array_map('trim', explode(',', $trusted_proxies)); + + // Check if the request comes from a trusted proxy IP + return in_array($remote_addr, $trusted_ips, true); + } + + /*shared*/ + /** + * Get the name of the HTTP header to use for getting the client IP from a reverse proxy + * @return string|null + */ + public function getProxyHeaderName() + { + global $prefs; + + // If a specific header is configured, use it + if (isset($prefs['feature_loadbalancer_header']) && ! empty($prefs['feature_loadbalancer_header'])) { + $header = strtoupper(str_replace('-', '_', $prefs['feature_loadbalancer_header'])); + // Add HTTP_ prefix if not already present + if (strpos($header, 'HTTP_') !== 0 && $header !== 'REMOTE_ADDR') { + $header = 'HTTP_' . $header; + } + return $header; + } + + return null; + } + /*shared*/ /** * @param $user ===================================== lib/validators/validator_uniqueemail.php ===================================== @@ -10,17 +10,11 @@ function validator_uniqueemail($input, $parameter = '', $message = '') { global $prefs; $userlib = TikiLib::lib('user'); + $tikilib = TikiLib::lib('tiki'); include_once __DIR__ . '/../../lib/ban/banlib.php'; - $ip = $_SERVER['REMOTE_ADDR']; - - if (! empty($_SERVER['HTTP_CLIENT_IP'])) { - $ip = $_SERVER['HTTP_CLIENT_IP']; - } elseif (! empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; - } elseif ($ip == '::1') { - $ip = gethostbyname(getHostName()); - } + // Use the secure get_ip_address() method which properly validates reverse proxy headers + $ip = $tikilib->get_ip_address(); $ban_message = tra('You are not allow to do such operation, please contact the administrator.'); ===================================== tiki-monitor.php ===================================== @@ -53,3 +53,235 @@ $result['SearchIndexRebuildLast'] = $tikilib->get_preference('unified_last_rebui $display = json_encode($result); echo $display; + +/** + * Check monitor is restricted by IP + * Security: Uses get_ip_address() which properly validates reverse proxy headers + * + * @return null + */ +function isMonitorRestrited() +{ + global $prefs; + $tikilib = TikiLib::lib('tiki'); + + $tikiMonitorRestriction = ! empty($prefs['monitor_restricted_ips']) ? explode(',', preg_replace('/\s+/', '', $prefs['monitor_restricted_ips'])) : []; + $sIpToCheck = null; + if (! empty($tikiMonitorRestriction)) { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && ! empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $aListIp = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $sIpToCheck = $aListIp[0]; + } elseif (isset($_SERVER['REMOTE_ADDR']) && ! empty($_SERVER['REMOTE_ADDR'])) { + $sIpToCheck = $_SERVER['REMOTE_ADDR']; + } + } + + if (in_array($sIpToCheck, $tikiMonitorRestriction) === false) { + header('location: index.php'); + exit(); + } +} + +/** + * Get monitor role based on token authentication + * + * @return string; + */ +function getMonitorRole() +{ + global $prefs, $tiki_p_admin; + + $role = 'public'; + if ($tiki_p_admin === 'y') { + $role = 'auth'; + } + + if (! empty($prefs['monitor_token'])) { + $requestMonitorToken = getRequestParam('monitoring_token', 'X-Tiki-Monitoring-Token'); + if ($prefs['monitor_token'] !== $requestMonitorToken) { + header("HTTP/1.1 401 Unauthorized"); + exit; + } + $role = 'auth'; + } + + return $role; +} + +/** + * Get default monitor authentication + * + * @return array + */ +function getDefaultMonitorRules() +{ + return [ + 'OPCodeCache:public', + 'OpCodeStats:public', + 'DbRequiresUpdate:public', + 'SearchIndexRebuildLast:public', + '*:auth' + ]; +} + +/** + * Get parameters from request or from header + * + * @param string $param + * @param string $headerParam + * @return string + */ +function getRequestParam($param, $headerParam) +{ + $requestParam = ! empty($_REQUEST[$param]) ? $_REQUEST[$param] : ''; + $allHeaders = getallheaders(); + if (! empty($allHeaders[$headerParam])) { + $requestParam = $allHeaders[$headerParam]; + } + + return $requestParam; +} + +/** + * Check monitor permission is valid + * + * @param string $monitor + * @return bool + */ +function isValidMonitor($monitor) +{ + global $prefs; + + $defaultMonitorRules = getDefaultMonitorRules(); + $monitorRules = ! empty($prefs['monitor_rules']) ? explode(PHP_EOL, $prefs['monitor_rules']) : $defaultMonitorRules; + foreach ($monitorRules as $authRule) { + $rule = ! empty($authRule) ? explode(':', $authRule) : null; + if (! empty($rule[0]) && ! empty($rule[1])) { + if ($rule[0] === '*') { + $rule[0] = $monitor; + } + if (str_contains($monitor, '.')) { + $subMonitor = explode('.', $monitor); + if (isValidRule($subMonitor, $rule[1], $rule[0])) { + return true; + } + } + if (str_contains($rule[0], '.')) { + $subRule = explode('.', $rule[0]); + if (isValidRule($subRule, $rule[1], $monitor)) { + return true; + } + } + if (isValidRule($rule[0], $rule[1], $monitor)) { + return true; + } + } + } + + return false; +} + +/** + * Check monitor rule + * + * @param string|array $rule + * @param string $ruleRole + * @param string $monitor + * @return bool + */ +function isValidRule($rule, $ruleRole, $monitor) +{ + $role = getMonitorRole(); + + if (empty($rule) || empty($ruleRole) || empty($monitor)) { + return false; + } + + if ( + (is_array($rule) && in_array($monitor, $rule) + || (is_string($rule) && $rule === $monitor)) + && ($ruleRole === 'public' || $ruleRole === 'auth' && $role === 'auth') + ) { + return true; + } + + return false; +} + +/** + * Get probes calculations + * + * @param array $result + * @return array + */ +function getProbes($result) +{ + global $prefs; + + $probes = []; + $probesList = ! empty($prefs['monitor_probes']) ? explode(PHP_EOL, $prefs['monitor_probes']) : []; + if (! empty($probesList)) { + $probes['result'] = "OK"; + + $runner = new Math_Formula_Runner( + [ + 'Math_Formula_Function_' => '', + 'Tiki_Formula_Function_' => '', + ] + ); + + $probesDetails = []; + foreach ($probesList as $line => $probe) { + $probeDetailLine = 'probe_' . ($line + 1); + $probesDetails[$probeDetailLine] = "OK"; + + try { + preg_match('/\(\S+\s+(\S+)\s+(\S+)\)/', $probe, $matches); + $probeMonitor = ! empty($matches[1]) ? $matches[1] : ''; + $probeMonitorValue = ! empty($matches[2]) ? $matches[2] : ''; + $monitorValue = isset($result[$probeMonitor]) ? $result[$probeMonitor] : ''; + + if (str_contains($probeMonitor, '.')) { + $subMonitor = explode('.', $probeMonitor); + $monitorValue = isset($result[$subMonitor[0]][$subMonitor[1]]) + ? $result[$subMonitor[0]][$subMonitor[1]] : ''; + } + if (empty($monitorValue)) { + $probesDetails[$probeDetailLine] = "FAIL"; + $probes['result'] = "FAIL"; + continue; + } + + // When value of monitor is a timestamp, probe value should be converted to timestamp + if ( + ! empty($probeMonitorValue) && is_numeric($probeMonitorValue) + && ((string) (int) $monitorValue === $monitorValue) + && ($monitorValue <= PHP_INT_MAX) + && ($monitorValue >= ~PHP_INT_MAX) + ) { + $dateTime = new DateTime(); + $dateTime->setTimestamp($monitorValue); + $dateTime->modify("+" . $probeMonitorValue . " minutes"); + $newTimestamp = $dateTime->getTimestamp(); + $probe = str_replace($probeMonitorValue, $newTimestamp, $probe); + } + if ($probeMonitorValue === 'NOW') { + $probe = str_replace($probeMonitorValue, time(), $probe); + } + $runner->setFormula($probe); + $runner->setVariables([$probeMonitor => $monitorValue]); + if (! $runner->evaluate()) { + $probesDetails[$probeDetailLine] = "FAIL"; + $probes['result'] = "FAIL"; + } + } catch (Math_Formula_Exception $e) { + $probes['result'] = "FAIL"; + $probesDetails[$probeDetailLine] = "FAIL"; + } + } + + $probes['details'] = $probesDetails; + } + + return $probes; +} View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1bb1f4f6e7bdc5efc9d60daa8c83daccf459f8c5 -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/1bb1f4f6e7bdc5efc9d60daa8c83daccf459f8c5 You're receiving this email because of your account on gitlab.com. _______________________________________________ TikiWiki-cvs mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs