diff options
author | Zebra North <mrzebra@mrzebra.co.uk> | 2022-10-22 14:04:18 +0100 |
---|---|---|
committer | Zebra North <mrzebra@mrzebra.co.uk> | 2022-10-22 14:04:18 +0100 |
commit | 608cdefc041020585a1fa262f9d0f99b700ff229 (patch) | |
tree | 210222c10e481647fa7d207e7758c9b5a25d2186 | |
parent | a75803218144699ad4af53f4d8b1a890b705a22c (diff) | |
download | dokuwiki-608cdefc041020585a1fa262f9d0f99b700ff229.tar.gz dokuwiki-608cdefc041020585a1fa262f9d0f99b700ff229.zip |
Factor out IP address functions; all proxies must be trusted
-rw-r--r-- | _test/tests/inc/common_clientip.test.php | 39 | ||||
-rw-r--r-- | _test/tests/inc/ip.test.php | 333 | ||||
-rw-r--r-- | inc/common.php | 16 | ||||
-rw-r--r-- | inc/ip.php | 218 | ||||
-rw-r--r-- | inc/load.php | 2 |
5 files changed, 579 insertions, 29 deletions
diff --git a/_test/tests/inc/common_clientip.test.php b/_test/tests/inc/common_clientip.test.php index 5e3dd69fb..4375ad921 100644 --- a/_test/tests/inc/common_clientip.test.php +++ b/_test/tests/inc/common_clientip.test.php @@ -2,12 +2,13 @@ class common_clientIP_test extends DokuWikiTest { - public function setup() : void { - parent::setup(); - - global $conf; - $conf['trustedproxy'] = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)'; - } + /** + * @var mixed[] $configs Possible values for $conf['trustedproxy']. + */ + private $configs = [ + '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)', + ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'], + ]; /** * The data provider for clientIP() tests. @@ -50,16 +51,16 @@ class common_clientIP_test extends DokuWikiTest { ['A', false, 'B', 'C,E', true, 'A'], ['A', true, 'B', 'C,E', true, 'B'], - // An X-Forwarded-For header with proxies from a trusted proxy. - ['D', false, 'B', 'C,E', false, 'C,E,D'], - ['D', true, 'B', 'C,E', false, 'B,C,E,D'], - ['D', false, 'B', 'C,E', true, 'C'], + // An X-Forwarded-For header with untrusted proxies from a trusted proxy. + ['D', false, 'B', 'C,E', false, 'D'], + ['D', true, 'B', 'C,E', false, 'B,D'], + ['D', false, 'B', 'C,E', true, 'D'], ['D', true, 'B', 'C,E', true, 'B'], // An X-Forwarded-For header with an invalid proxy from a trusted proxy. - ['D', false, 'B', 'C,invalid,E', false, 'C,E,D'], - ['D', true, 'B', 'C,invalid,E', false, 'B,C,E,D'], - ['D', false, 'B', 'C,invalid,E', true, 'C'], + ['D', false, 'B', 'C,invalid,E', false, 'D'], + ['D', true, 'B', 'C,invalid,E', false, 'B,D'], + ['D', false, 'B', 'C,invalid,E', true, 'D'], ['D', true, 'B', 'C,invalid,E', true, 'B'], // Malicious X-Real-IP and X-Forwarded-For headers. @@ -108,7 +109,10 @@ class common_clientIP_test extends DokuWikiTest { $_SERVER['HTTP_X_FORWARDED_FOR'] = str_replace(array_keys($addresses), array_values($addresses), $forwardedFor); $conf['realip'] = $useRealIp; - $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single)); + foreach ($this->configs as $config) { + $conf['trustedproxy'] = $config; + $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single)); + } } /** @@ -141,8 +145,9 @@ class common_clientIP_test extends DokuWikiTest { $_SERVER['HTTP_X_FORWARDED_FOR'] = str_replace(array_keys($addresses), array_values($addresses), $forwardedFor); $conf['realip'] = $useRealIp; - $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single)); + foreach ($this->configs as $config) { + $conf['trustedproxy'] = $config; + $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single)); + } } } - -//Setup VIM: ex: et ts=4 : diff --git a/_test/tests/inc/ip.test.php b/_test/tests/inc/ip.test.php new file mode 100644 index 000000000..ea942031d --- /dev/null +++ b/_test/tests/inc/ip.test.php @@ -0,0 +1,333 @@ +<?php + +class ip_test extends DokuWikiTest { + + /** + * The data provider for ipToNumber() tests. + * + * @return mixed[][] Returns an array of test cases. + */ + public function ip_to_number_provider() : array + { + $tests = [ + ['127.0.0.1', 4, 0x00000000, 0x7f000001], + ['::127.0.0.1', 6, 0x00000000, 0x7f000001], + ['::1', 6, 0x00000000, 0x00000001], + ['38AF:3033:AA39:CDE3:1A46:094C:44ED:5300', 6, 0x38AF3033AA39CDE3, 0x1A46094C44ED5300], + ['193.53.125.7', 4, 0x00000000, 0xC1357D07], + ]; + + return $tests; + } + + /** + * Test ipToNumber(). + * + * @dataProvider ip_to_number_provider + * + * @param string $ip The IP address to convert. + * @param int $version The IP version, either 4 or 6. + * @param int $upper The upper 64 bits of the IP. + * @param int $lower The lower 64 bits of the IP. + * + * @return void + */ + public function test_ip_to_number(string $ip, int $version, int $upper, int $lower): void + { + $result = dokuwiki\ipToNumber($ip); + + $this->assertSame($version, $result['version']); + $this->assertSame($upper, $result['upper']); + $this->assertSame($lower, $result['lower']); + } + + /** + * The data provider for test_ip_in_range(). + * + * @return mixed[][] Returns an array of test cases. + */ + public function ip_in_range_provider(): array + { + $tests = [ + ['192.168.11.2', '192.168.0.0/16', true], + ['192.168.11.2', '192.168.64.1/16', true], + ['192.168.11.2', '192.168.64.1/18', false], + ['192.168.11.2', '192.168.11.0/20', true], + ['127.0.0.1', '127.0.0.0/7', true], + ['127.0.0.1', '127.0.0.0/8', true], + ['127.0.0.1', '127.200.0.0/8', true], + ['127.0.0.1', '127.200.0.0/9', false], + ['127.0.0.1', '127.0.0.0/31', true], + ['127.0.0.1', '127.0.0.0/32', false], + ['127.0.0.1', '127.0.0.1/32', true], + ['1111:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true], + ['1110:2222:3333:4444:5555:6666:7777:8888', '1110::/12', true], + ['1100:2222:3333:4444:5555:6666:7777:8888', '1110::/12', false], + ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3300::/40', true], + ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3200::/40', false], + ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/127', true], + ['1111:2222:3333:4444:5555:6666:7777:8888', '1111:2222:3333:4444:5555:6666:7777:8889/128', false], + ['1111:2222:3333:4444:5555:6666:7777:8889', '1111:2222:3333:4444:5555:6666:7777:8889/128', true], + ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', true], + ['abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abce', 'abcd:ef0a:bcde:f0ab:cdef:0abc:def0:abcd/128', false], + ]; + + return $tests; + } + + /** + * Test ipInRange(). + * + * @dataProvider ip_in_range_provider + * + * @param string $ip The IP to test. + * @param string $range The IP range to test against. + * @param bool $expected The expected result from ipInRange(). + * + * @return void + */ + public function test_ip_in_range(string $ip, string $range, bool $expected): void + { + $result = dokuwiki\ipInRange($ip, $range); + + $this->assertSame($expected, $result); + } + + /** + * Data provider for test_ip_matches(). + * + * @return mixed[][] Returns an array of test cases. + */ + public function ip_matches_provider(): array + { + // Tests for a CIDR range. + $rangeTests = $this->ip_in_range_provider(); + + // Tests for an exact IP match. + $exactTests = [ + ['127.0.0.1', '127.0.0.1', true], + ['127.0.0.1', '127.0.0.0', false], + ['aaaa:bbbb:cccc:dddd:eeee::', 'aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', true], + ['aaaa:bbbb:cccc:dddd:eeee:0000:0000:0000', 'aaaa:bbbb:cccc:dddd:eeee::', true], + ['aaaa:bbbb:0000:0000:0000:0000:0000:0001', 'aaaa:bbbb::1', true], + ['aaaa:bbbb::0001', 'aaaa:bbbb::1', true], + ['aaaa:bbbb::0001', 'aaaa:bbbb::', false], + ['::ffff:127.0.0.1', '127.0.0.1', false], + ['::ffff:127.0.0.1', '::0:ffff:127.0.0.1', true], + ]; + + + return array_merge($rangeTests, $exactTests); + } + + /** + * Test ipMatches(). + * + * @dataProvider ip_matches_provider + * + * @param string $ip The IP to test. + * @param string $ipOrRange The IP or IP range to test against. + * @param bool $expected The expeced result from ipMatches(). + * + * @return void + */ + public function test_ip_matches(string $ip, string $ipOrRange, bool $expected): void + { + $result = dokuwiki\ipMatches($ip, $ipOrRange); + + $this->assertSame($expected, $result); + } + + /** + * Data provider for proxyIsTrusted(). + * + * @return mixed[][] Returns an array of test cases. + */ + public function proxy_is_trusted_provider(): array + { + // The default value that shipped with the config. + $legacyDefault = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)'; + + // The new default configuration value. + $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; + + // Adding some custom trusted proxies. + $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']); + + $tests = [ + // Empty configuration. + ['', '127.0.0.1', false], + + // Legacy configuration with a regular expression. + [$legacyDefault, '127.0.0.1', true], + [$legacyDefault, '127.1.2.3', true], + [$legacyDefault, '10.1.2.3', true], + [$legacyDefault, '11.1.2.3', false], + [$legacyDefault, '172.16.0.1', true], + [$legacyDefault, '172.160.0.1', false], + [$legacyDefault, '172.31.255.255', true], + [$legacyDefault, '172.32.0.0', false], + [$legacyDefault, '172.200.0.0', false], + [$legacyDefault, '192.168.2.3', true], + [$legacyDefault, '192.169.1.2', false], + [$legacyDefault, '::1', true], + + // Configuration with an array of IPs/CIDRs. + [$default, '127.0.0.1', true], + [$default, '127.1.2.3', true], + [$default, '10.1.2.3', true], + [$default, '11.1.2.3', false], + [$default, '172.16.0.1', true], + [$default, '172.160.0.1', false], + [$default, '172.31.255.255', true], + [$default, '172.32.0.0', false], + [$default, '172.200.0.0', false], + [$default, '192.168.2.3', true], + [$default, '192.169.1.2', false], + [$default, '::1', true], + [$default, '0000:0000:0000:0000:0000:0000:0000:0001', true], + + // With custom proxies set. + [$custom, '127.0.0.1', true], + [$custom, '1.2.3.4', true], + [$custom, '3.0.1.2', true], + [$custom, '1122::', true], + [$custom, '1122:0000:0000:0000:0000:0000:0000:0000', true], + [$custom, '1111:2223::', false], + [$custom, '1111:2222::', true], + [$custom, '1111:2222:3333::', true], + [$custom, '1111:2222:3333::1', true], + ]; + + return $tests; + } + + /** + * Test proxyIsTrusted(). + * + * @dataProvider proxy_is_trusted_provider + * + * @param string|string[] $config The value for $conf[trustedproxy]. + * @param string $ip The proxy IP to test. + * @param bool $expected The expected result from proxyIsTrusted(). + */ + public function test_proxy_is_trusted($config, string $ip, bool $expected): void + { + global $conf; + $conf['trustedproxy'] = $config; + + $result = dokuwiki\proxyIsTrusted($ip); + + $this->assertSame($expected, $result); + } + + /** + * Data provider for test_forwarded_for(). + * + * @return mixed[][] Returns an array of test cases. + */ + public function forwarded_for_provider(): array + { + // The default value that shipped with the config. + $legacyDefault = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)'; + + // The new default configuration value. + $default = ['::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; + + // Adding some custom trusted proxies. + $custom = array_merge($default, ['1.2.3.4', '1122::', '3.0.0.1/8', '1111:2222::/32']); + + $tests = [ + // Empty config value should always return empty array. + ['', '', '127.0.0.1', []], + ['', '127.0.0.1', '127.0.0.1', []], + [[], '', '127.0.0.1', []], + [[], '127.0.0.1', '127.0.0.1', []], + + // The old default configuration. + [$legacyDefault, '', '127.0.0.1', []], + [$legacyDefault, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']], + [$legacyDefault, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']], + [$legacyDefault, '1.2.3.4,172.16.0.1', '192.168.1.1', ['1.2.3.4', '172.16.0.1', '192.168.1.1']], + [$legacyDefault, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']], + + // Directly from an untrusted proxy. + [$legacyDefault, '', '127.0.0.1', []], + [$legacyDefault, '1.2.3.4', '11.22.33.44', []], + [$legacyDefault, '::1', '11.22.33.44', []], + + // From a trusted proxy, but via an untrusted proxy. + [$legacyDefault, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []], + [$legacyDefault, '1.2.3.4,::2,172.16.0.1', '::1', []], + + // The new default configuration. + [$default, '', '127.0.0.1', []], + [$default, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']], + [$default, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']], + [$default, '1.2.3.4,172.16.0.1', '192.168.1.1', ['1.2.3.4', '172.16.0.1', '192.168.1.1']], + [$default, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']], + [$default, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']], + + // Directly from an untrusted proxy. + [$default, '', '127.0.0.1', []], + [$default, '1.2.3.4', '11.22.33.44', []], + [$default, '::1', '11.22.33.44', []], + [$default, '::1', '::2', []], + + // From a trusted proxy, but via an untrusted proxy. + [$default, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []], + [$default, '1.2.3.4,::2,172.16.0.1', '::1', []], + + // A custom configuration. + [$custom, '', '127.0.0.1', []], + [$custom, '1.2.3.4', '127.0.0.1', ['1.2.3.4', '127.0.0.1']], + [$custom, '1.2.3.4', '192.168.1.1', ['1.2.3.4', '192.168.1.1']], + [$custom, '1.2.3.4,172.16.0.1', '192.168.1.1', ['1.2.3.4', '172.16.0.1', '192.168.1.1']], + [$custom, '1.2.3.4,172.16.0.1', '::1', ['1.2.3.4', '172.16.0.1', '::1']], + [$custom, '1.2.3.4,172.16.0.1', '::0001', ['1.2.3.4', '172.16.0.1', '::0001']], + + // Directly from an untrusted proxy. + [$custom, '', '127.0.0.1', []], + [$custom, '1.2.3.4', '11.22.33.44', []], + [$custom, '::1', '11.22.33.44', []], + [$custom, '::1', '::2', []], + + // From a trusted proxy, but via an untrusted proxy. + [$custom, '1.2.3.4,11.22.33.44,172.16.0.1', '192.168.1.1', []], + [$custom, '1.2.3.4,::2,172.16.0.1', '::1', []], + + // Via a custom proxy. + [$custom, '11.2.3.4,3.1.2.3,172.16.0.1', '192.168.1.1', ['11.2.3.4', '3.1.2.3', '172.16.0.1', '192.168.1.1']], + [$custom, '11.2.3.4,1122::,172.16.0.1', '3.0.0.1', ['11.2.3.4', '1122::', '172.16.0.1', '3.0.0.1']], + [$custom, '11.2.3.4,1122::,172.16.0.1', '1111:2222:3333::', ['11.2.3.4', '1122::', '172.16.0.1', '1111:2222:3333::']], + ]; + + return $tests; + } + + /** + * Test forwardedFor(). + * + * @dataProvider forwarded_for_provider + * + * @param string|string[] $config The trustedproxy config value. + * @param string $header The X-Forwarded-For header value. + * @param string $remoteAddr The TCP/IP peer address. + * @param array $expected The expected result from forwardedFor(). + * + * @return void + */ + public function test_forwarded_for($config, string $header, string $remoteAddr, array $expected): void + { + /* @var Input $INPUT */ + global $INPUT, $conf; + + $conf['trustedproxy'] = $config; + $INPUT->server->set('HTTP_X_FORWARDED_FOR', $header); + $INPUT->server->set('REMOTE_ADDR', $remoteAddr); + + $result = dokuwiki\forwardedFor(); + + $this->assertSame($expected, $result); + } +} diff --git a/inc/common.php b/inc/common.php index 5d245e9b8..527a98d64 100644 --- a/inc/common.php +++ b/inc/common.php @@ -780,7 +780,8 @@ function checkwordblock($text = '') { * * @author Zebra North <mrzebra@mrzebra.co.uk> * - * @param boolean $single If set only a single IP is returned + * @param bool $single If set only a single IP is returned. + * * @return string Returns an IP address if 'single' is true, or a comma-separated list * of IP addresses otherwise. */ @@ -796,18 +797,11 @@ function clientIP($single = false) { $ips[] = $INPUT->server->str('HTTP_X_REAL_IP'); } - // Get the client address from the X-Forwarded-For header. - // X-Forwarded-For: <client> [, <proxy>]... - $forwardedFor = explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))); - $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); - - // Add the X-Forwarded-For address if the header was set by a trusted proxy. - if ($forwardedFor[0] && !empty($conf['trustedproxy']) && preg_match('/' . $conf['trustedproxy'] . '/', $remoteAddr)) { - $ips = array_merge($ips, $forwardedFor); - } + // Add the X-Forwarded-For addresses if all proxies are trusted. + $ips = array_merge($ips, dokuwiki\forwardedFor()); // Add the TCP/IP connection endpoint. - $ips[] = $remoteAddr; + $ips[] = $INPUT->server->str('REMOTE_ADDR'); // Remove invalid IPs. $ips = array_filter($ips, function ($ip) { return filter_var($ip, FILTER_VALIDATE_IP); }); diff --git a/inc/ip.php b/inc/ip.php new file mode 100644 index 000000000..c025f6fc3 --- /dev/null +++ b/inc/ip.php @@ -0,0 +1,218 @@ +<?php +/** + * DokuWiki IP address functions. + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Zebra North <mrzebra@mrzebra.co.uk> + */ + +namespace dokuwiki; + +use Exception; + +/** + * Determine whether an IP address is within a given CIDR range. + * The needle and haystack may be either IPv4 or IPv6. + * + * Example: + * + * ipInRange('192.168.11.123', '192.168.0.0/16') === true + * ipInRange('192.168.11.123', '::192.168.0.0/80') === true + * ipInRange('::192.168.11.123', '192.168.0.0/16') === true + * ipInRange('::192.168.11.123', '::192.168.0.0/80') === true + * + * @param string $needle The IP to test, either IPv4 in doted decimal + * notation or IPv6 in colon notation. + * @param string $haystack The CIDR range as an IP followed by a forward + * slash and the number of significant bits. + * + * @return bool Returns true if $needle is within the range specified + * by $haystack, false if it is outside the range. + * + * @throws Exception Thrown if $needle is not a valid IP address. + * @throws Exception Thrown if $haystack is not a valid IP range. + */ +function ipInRange(string $needle, string $haystack): bool +{ + $range = explode('/', $haystack); + $networkIp = ipToNumber($range[0]); + $maskLength = $range[1]; + + // For an IPv4 address the top 96 bits must be zero. + if ($networkIp['version'] === 4) { + $maskLength += 96; + } + + if ($maskLength > 128) { + throw new Exception('Invalid IP range mask: ' . $haystack); + } + + $maskLengthUpper = min($maskLength, 64); + $maskLengthLower = max(0, $maskLength - 64); + + $maskUpper = ~0 << intval(64 - $maskLengthUpper); + $maskLower = ~0 << intval(64 - $maskLengthLower); + + $needle = ipToNumber($needle); + + return ($needle['upper'] & $maskUpper) === ($networkIp['upper'] & $maskUpper) && + ($needle['lower'] & $maskLower) === ($networkIp['lower'] & $maskLower); +} + +/** + * Convert an IP address from a string to a number. + * + * This splits 128 bit IP addresses into the upper and lower 64 bits, and + * also returns whether the IP given was IPv4 or IPv6. + * + * The returned array contains: + * + * - version: Either '4' or '6'. + * - upper: The upper 64 bits of the IP. + * - lower: The lower 64 bits of the IP. + * + * For an IPv4 address, 'upper' will always be zero. + * + * @param string The IPv4 or IPv6 address. + * + * @return int[] Returns an array of 'version', 'upper', 'lower'. + * + * @throws Exception Thrown if the IP is not valid. + */ +function ipToNumber(string $ip): array +{ + $binary = inet_pton($ip); + + if ($binary === false) { + throw new Exception('Invalid IP: ' . $ip); + } + + if (strlen($binary) === 4) { + // IPv4. + return [ + 'version' => 4, + 'upper' => 0, + 'lower' => unpack('Nip', $binary)['ip'], + ]; + } else { + // IPv6. + $result = unpack('Jupper/Jlower', $binary); + $result['version'] = 6; + return $result; + } +} + +/** + * Determine if an IP address is equal to another IP or within an IP range. + * IPv4 and IPv6 are supported. + * + * @param string $ip The address to test. + * @param string $ipOrRange An IP address or CIDR range. + * + * @return bool Returns true if the IP matches, false if not. + */ +function ipMatches(string $ip, string $ipOrRange): bool +{ + try { + // If it's not a range, compare the addresses directly. + // Addresses are converted to numbers because the same address may be + // represented by different strings, e.g. "::1" and "::0001". + if (strpos($ipOrRange, '/') === false) { + return ipToNumber($ip) === ipToNumber($ipOrRange); + } + + return ipInRange($ip, $ipOrRange); + } catch (Exception $ex) { + // The IP address was invalid. + return false; + } +} + +/** + * Given the IP address of a proxy server, determine whether it is + * a known and trusted server. + * + * This test is performed using the config value `trustedproxy`. + * + * @param string $ip The IP address of the proxy. + * + * @return bool Returns true if the IP is trusted as a proxy. + */ +function proxyIsTrusted(string $ip): bool +{ + global $conf; + + // If the configuration is empty then no proxies are trusted. + if (empty($conf['trustedproxy'])) { + return false; + } + + if (is_string($conf['trustedproxy'])) { + // If the configuration is a string then treat it as a regex. + return preg_match('/' . $conf['trustedproxy'] . '/', $ip); + } elseif (is_array($conf['trustedproxy'])) { + // If the configuration is an array, then at least one must match. + foreach ($conf['trustedproxy'] as $trusted) { + if (ipMatches($ip, $trusted)) { + return true; + } + } + + return false; + } + + throw new Exception('Invalid value for $conf[trustedproxy]'); +} + +/** + * Get the originating IP address and the address of every proxy that the + * request has passed through, according to the X-Forwarded-For header. + * + * To prevent spoofing of the client IP, every proxy listed in the + * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint + * from which the connection was received (i.e. the final proxy). + * + * If the header is not present or contains an untrusted proxy then + * an empty array is returned. + * + * The client IP is the first entry in the returned list, followed by the + * proxies. + * + * @return string[] Returns an array of IP addresses. + */ +function forwardedFor(): array +{ + /* @var Input $INPUT */ + global $INPUT, $conf; + + $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR'); + + if (empty($conf['trustedproxy']) || !$forwardedFor) { + return []; + } + + // This is the address from which the header was received. + $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); + + // Get the client address from the X-Forwarded-For header. + // X-Forwarded-For: <client> [, <proxy>]... + $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor)); + + // The client address is the first item, remove it from the list. + $clientAddress = array_shift($forwardedFor); + + // The remaining items are the proxies through which the X-Forwarded-For + // header has passed. The final proxy is the connection's remote address. + $proxies = $forwardedFor; + $proxies[] = $remoteAddr; + + // Ensure that every proxy is trusted. + foreach ($proxies as $proxy) { + if (!proxyIsTrusted($proxy)) { + return []; + } + } + + // Add the client address before the list of proxies. + return array_merge([$clientAddress], $proxies); +} diff --git a/inc/load.php b/inc/load.php index 022ac014f..8b298fa84 100644 --- a/inc/load.php +++ b/inc/load.php @@ -25,6 +25,7 @@ require_once(DOKU_INC.'inc/httputils.php'); require_once(DOKU_INC.'inc/indexer.php'); require_once(DOKU_INC.'inc/infoutils.php'); require_once(DOKU_INC.'inc/io.php'); +require_once(DOKU_INC.'inc/ip.php'); require_once(DOKU_INC.'inc/mail.php'); require_once(DOKU_INC.'inc/media.php'); require_once(DOKU_INC.'inc/pageutils.php'); @@ -153,4 +154,3 @@ function load_autoload($name){ } return false; } - |