aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorAndreas Gohr <andi@splitbrain.org>2025-01-09 18:45:43 +0100
committerGitHub <noreply@github.com>2025-01-09 18:45:43 +0100
commit26e58b9a3e7454743ebd86a977a72eaba77d9da9 (patch)
tree991d7e68e4f5bb378c604e72b1b03b1fe23acd41
parent7a8a36a297106998b5de3ce6839475af05ad48de (diff)
parentced0b55f80d7b970652d113461ee14228babe506 (diff)
downloaddokuwiki-26e58b9a3e7454743ebd86a977a72eaba77d9da9.tar.gz
dokuwiki-26e58b9a3e7454743ebd86a977a72eaba77d9da9.zip
Merge pull request #4390 from dokuwiki/clientIP
clientIP handling
-rw-r--r--_test/tests/inc/Ip.test.php297
-rw-r--r--_test/tests/inc/common_clientip.test.php355
-rw-r--r--conf/dokuwiki.php12
-rw-r--r--inc/Ip.php283
-rw-r--r--inc/common.php65
-rw-r--r--lib/plugins/config/lang/en/lang.php3
-rw-r--r--lib/plugins/config/settings/config.metadata.php3
7 files changed, 754 insertions, 264 deletions
diff --git a/_test/tests/inc/Ip.test.php b/_test/tests/inc/Ip.test.php
new file mode 100644
index 000000000..4809f6f6d
--- /dev/null
+++ b/_test/tests/inc/Ip.test.php
@@ -0,0 +1,297 @@
+<?php
+
+use dokuwiki\Ip;
+
+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 = Ip::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 = Ip::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 = Ip::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 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],
+
+ // 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[trustedproxies].
+ * @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['trustedproxies'] = $config;
+
+ $result = Ip::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 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', []],
+
+ // 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 trustedproxies 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['trustedproxies'] = $config;
+ $INPUT->server->set('HTTP_X_FORWARDED_FOR', $header);
+ $INPUT->server->set('REMOTE_ADDR', $remoteAddr);
+
+ $result = Ip::forwardedFor();
+
+ $this->assertSame($expected, $result);
+ }
+}
diff --git a/_test/tests/inc/common_clientip.test.php b/_test/tests/inc/common_clientip.test.php
index 212d8cfbc..f98909c50 100644
--- a/_test/tests/inc/common_clientip.test.php
+++ b/_test/tests/inc/common_clientip.test.php
@@ -2,224 +2,151 @@
class common_clientIP_test extends DokuWikiTest {
- function setup() : void {
- parent::setup();
-
+ /**
+ * @var mixed[] $configs Possible values for $conf['trustedproxies'].
+ */
+ private $configs = [
+ ['::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.
+ *
+ * @return mixed[][] Returns an array of test cases.
+ */
+ public function client_ip_all_provider() : array {
+ // Malicious code in a header.
+ $bad = '<?php die("hacked"); ?>';
+
+ // Letters A, B, C, D, E will be substitued with an IPv4 or IPv6 address.
+ $tests = [
+ // A single IP with no other headers.
+ ['A', false, '', '', false, 'A'],
+ ['A', true, '', '', false, 'A'],
+ ['A', false, '', '', true, 'A'],
+ ['A', true, '', '', true, 'A'],
+
+ // A X-Real-IP header.
+ ['A', false, 'B', '', false, 'A'],
+ ['A', true, 'B', '', false, 'B,A'],
+ ['A', false, 'B', '', true, 'A'],
+ ['A', true, 'B', '', true, 'B'],
+
+ // An X-Forwarded-For header from an untrusted proxy.
+ ['A', false, 'B', 'C', false, 'A'],
+ ['A', true, 'B', 'C', false, 'B,A'],
+ ['A', false, 'B', 'C', true, 'A'],
+ ['A', true, 'B', 'C', true, 'B'],
+
+ // An X-Forwarded-For header from a trusted proxy.
+ ['D', false, 'B', 'C', false, 'C,D'],
+ ['D', true, 'B', 'C', false, 'B,C,D'],
+ ['D', false, 'B', 'C', true, 'C'],
+ ['D', true, 'B', 'C', true, 'B'],
+
+ // An X-Forwarded-For header with proxies from an untrusted proxy.
+ ['A', false, 'B', 'C,E', false, 'A'],
+ ['A', true, 'B', 'C,E', false, 'B,A'],
+ ['A', false, 'B', 'C,E', true, 'A'],
+ ['A', true, 'B', 'C,E', true, 'B'],
+
+ // 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, '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.
+ ['A', false, $bad, $bad, false, 'A'],
+ ['A', true, $bad, $bad, false, 'A'],
+ ['A', false, $bad, $bad, true, 'A'],
+ ['A', true, $bad, $bad, true, 'A'],
+
+ // Malicious remote address, X-Real-IP and X-Forwarded-For headers.
+ [$bad, false, $bad, $bad, false, '0.0.0.0'],
+ [$bad, true, $bad, $bad, false, '0.0.0.0'],
+ [$bad, false, $bad, $bad, true, '0.0.0.0'],
+ [$bad, true, $bad, $bad, true, '0.0.0.0'],
+ ];
+
+ return $tests;
+ }
+
+ /**
+ * Test clientIP() with IPv6 addresses.
+ *
+ * @dataProvider client_ip_all_provider
+ *
+ * @param string $remoteAddr The TCP/IP remote IP address.
+ * @param bool $useRealIp True if using the X-Real-IP header is enabled in the config.
+ * @param string $realIp The X-Real-IP header.
+ * @param string $forwardedFor The X-Forwarded-For header.
+ * @param bool $single True to return the most likely client IP, false to return all candidates.
+ * @param string $expected The expected function result.
+ *
+ * @return void
+ */
+ public function test_client_ip_v4(string $remoteAddr, bool $useRealIp, string $realIp, string $forwardedFor, bool $single, string $expected) : void {
global $conf;
- $conf['trustedproxy'] = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)';
- }
-
- function test_simple_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP());
- }
-
- function test_proxy1_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '77.77.77.77';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '123.123.123.123,77.77.77.77';
- $this->assertEquals($out, clientIP());
- }
-
- function test_proxy2_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77';
- $out = '123.123.123.123,77.77.77.77';
- $this->assertEquals($out, clientIP());
- }
-
- function test_proxyhops_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77,66.66.66.66';
- $out = '123.123.123.123,77.77.77.77,66.66.66.66';
- $this->assertEquals($out, clientIP());
- }
-
- function test_simple_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxy1_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '77.77.77.77';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxy2_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxyhops_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77,66.66.66.66';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxy1_local_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '77.77.77.77';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '77.77.77.77';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxy2_local_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77';
- $out = '77.77.77.77';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxyhops1_local_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '77.77.77.77,66.66.66.66';
- $out = '77.77.77.77';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxyhops2_local_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,66.66.66.66';
- $out = '66.66.66.66';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_local_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
- $out = '123.123.123.123,127.0.0.1';
- $this->assertEquals($out, clientIP());
- }
-
- function test_local1_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_local2_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_local3_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,10.0.0.1,192.168.0.2,172.17.1.1,172.21.1.1,172.31.1.1';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_local4_single(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '192.168.0.5';
- $out = '192.168.0.5';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_garbage_all(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = 'some garbage, or something, 222';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP());
- }
-
- function test_garbage_single(){
- $_SERVER['REMOTE_ADDR'] = '123.123.123.123';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = 'some garbage, or something, 222';
- $out = '123.123.123.123';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_garbageonly_all(){
- $_SERVER['REMOTE_ADDR'] = 'argh';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = 'some garbage, or something, 222';
- $out = '0.0.0.0';
- $this->assertEquals($out, clientIP());
- }
-
- function test_garbageonly_single(){
- $_SERVER['REMOTE_ADDR'] = 'argh';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = 'some garbage, or something, 222';
- $out = '0.0.0.0';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_malicious(){
- $_SERVER['REMOTE_ADDR'] = '';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '<?php set_time_limit(0);echo \'my_delim\';passthru(123.123.123.123);die;?>';
- $out = '0.0.0.0';
- $this->assertEquals($out, clientIP());
- }
-
- function test_malicious_with_remote_addr(){
- $_SERVER['REMOTE_ADDR'] = '8.8.8.8';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '<?php set_time_limit(0);echo \'my_delim\';passthru(\',123.123.123.123,\');die;?>';
- $out = '8.8.8.8';
- $this->assertEquals($out, clientIP(true));
- }
-
- function test_proxied_malicious_with_remote_addr(){
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '8.8.8.8,<?php set_time_limit(0);echo \'my_delim\';passthru(\',123.123.123.123,\');die;?>';
- $out = '127.0.0.1,8.8.8.8,123.123.123.123';
- $this->assertEquals($out, clientIP());
- }
+ $addresses = [
+ 'A' => '123.123.123.123',
+ 'B' => '22.22.22.22',
+ 'C' => '33.33.33.33',
+ 'D' => '192.168.11.1',
+ 'E' => '44.44.44.44',
+ ];
+
+ $_SERVER['REMOTE_ADDR'] = str_replace(array_keys($addresses), array_values($addresses), $remoteAddr);
+ $_SERVER['HTTP_X_REAL_IP'] = str_replace(array_keys($addresses), array_values($addresses), $realIp);
+ $_SERVER['HTTP_X_FORWARDED_FOR'] = str_replace(array_keys($addresses), array_values($addresses), $forwardedFor);
+ $conf['realip'] = $useRealIp;
+
+ foreach ($this->configs as $config) {
+ $conf['trustedproxies'] = $config;
+ $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single));
+ }
+ }
+
+ /**
+ * Test clientIP() with IPv6 addresses.
+ *
+ * @dataProvider client_ip_all_provider
+ *
+ * @param string $remoteAddr The TCP/IP remote IP address.
+ * @param bool $useRealIp True if using the X-Real-IP header is enabled in the config.
+ * @param string $realIp The X-Real-IP header.
+ * @param string $forwardedFor The X-Forwarded-For header.
+ * @param bool $single True to return the most likely client IP, false to return all candidates.
+ * @param string $expected The expected function result.
+ *
+ * @return void
+ */
+ public function test_client_ip_v6(string $remoteAddr, bool $useRealIp, string $realIp, string $forwardedFor, bool $single, string $expected) : void {
+ global $conf;
- // IPv6
+ $addresses = [
+ 'A' => '1234:1234:1234:1234:1234:1234:1234:1234',
+ 'B' => '22:aa:22:bb:22:cc:22:dd',
+ 'C' => '33:aa:33:bb:33:cc:33:dd',
+ 'D' => '::1',
+ 'E' => '44:aa:44:bb:44:cc:44:dd',
+ ];
- function test_simple_single_ipv6(){
- $_SERVER['REMOTE_ADDR'] = '1234:1234:1234:1234:1234:1234:1234:1234';
- $_SERVER['HTTP_X_REAL_IP'] = '';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
- $out = '1234:1234:1234:1234:1234:1234:1234:1234';
- $this->assertEquals($out, clientIP(true));
- }
+ $_SERVER['REMOTE_ADDR'] = str_replace(array_keys($addresses), array_values($addresses), $remoteAddr);
+ $_SERVER['HTTP_X_REAL_IP'] = str_replace(array_keys($addresses), array_values($addresses), $realIp);
+ $_SERVER['HTTP_X_FORWARDED_FOR'] = str_replace(array_keys($addresses), array_values($addresses), $forwardedFor);
+ $conf['realip'] = $useRealIp;
- function test_proxyhops_garbage_all_ipv4_and_ipv6(){
- $_SERVER['REMOTE_ADDR'] = '1234:1234:1234:1234:1234:1234:1234:1234';
- $_SERVER['HTTP_X_REAL_IP'] = '1.1.1.1';
- $_SERVER['HTTP_X_FORWARDED_FOR'] = '777:777:777:777:777:777:777:777,::1,skipme,66.66.66.66';
- $out = '1234:1234:1234:1234:1234:1234:1234:1234,777:777:777:777:777:777:777:777,::1,66.66.66.66,1.1.1.1';
- $this->assertEquals($out, clientIP());
+ foreach ($this->configs as $config) {
+ $conf['trustedproxies'] = $config;
+ $this->assertEquals(str_replace(array_keys($addresses), array_values($addresses), $expected), clientIP($single));
+ }
}
-
}
-
-//Setup VIM: ex: et ts=4 :
diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php
index 9cb03575e..6990b23e4 100644
--- a/conf/dokuwiki.php
+++ b/conf/dokuwiki.php
@@ -161,9 +161,6 @@ $conf['renderer_xhtml'] = 'xhtml'; //renderer to use for main page generat
$conf['readdircache'] = 0; //time cache in second for the readdir operation, 0 to deactivate.
$conf['search_nslimit'] = 0; //limit the search to the current X namespaces
$conf['search_fragment'] = 'exact'; //specify the default fragment search behavior
-$conf['trustedproxy'] = '^(::1|[fF][eE]80:|127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)';
- //Regexp of trusted proxy address when reading IP using HTTP header
- // if blank, do not trust any proxy (including local IP)
/* Feature Flags */
$conf['defer_js'] = 1; // Defer javascript to be executed after the page's HTML has been parsed. Setting will be removed in the next release.
@@ -172,6 +169,15 @@ $conf['hidewarnings'] = 0; // Hide warnings
/* Network Settings */
$conf['dnslookups'] = 1; //disable to disallow IP to hostname lookups
$conf['jquerycdn'] = 0; //use a CDN for delivering jQuery?
+$conf['trustedproxies'] = array('::1', 'fe80::/10', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16');
+ // Trusted proxy servers from which to read the X-Forwarded-For header.
+ // Each item in the array may be either an IPv4 or IPv6 address, or
+ // an IPv4 or IPv6 CIDR range (e.g. 10.0.0.0/8).
+
+$conf['realip'] = false; // Enable reading the X-Real-IP header. Default: false.
+ // Only enable this if your server writes this header, otherwise it may be spoofed.
+
+
// Proxy setup - if your Server needs a proxy to access the web set these
$conf['proxy']['host'] = '';
$conf['proxy']['port'] = '';
diff --git a/inc/Ip.php b/inc/Ip.php
new file mode 100644
index 000000000..4d4ee6ca6
--- /dev/null
+++ b/inc/Ip.php
@@ -0,0 +1,283 @@
+<?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 dokuwiki\Input\Input;
+use Exception;
+
+class Ip
+{
+ /**
+ * 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 dotted 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.
+ */
+ public static function ipInRange(string $needle, string $haystack): bool
+ {
+ $range = explode('/', $haystack);
+ $networkIp = Ip::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 = Ip::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 $ip The IPv4 or IPv6 address.
+ *
+ * @return int[] Returns an array of 'version', 'upper', 'lower'.
+ *
+ * @throws Exception Thrown if the IP is not valid.
+ */
+ public static 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.
+ */
+ public static 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 Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange);
+ }
+
+ return Ip::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 `trustedproxies`.
+ *
+ * @param string $ip The IP address of the proxy.
+ *
+ * @return bool Returns true if the IP is trusted as a proxy.
+ */
+ public static function proxyIsTrusted(string $ip): bool
+ {
+ global $conf;
+
+ // If the configuration is empty then no proxies are trusted.
+ if (empty($conf['trustedproxies'])) {
+ return false;
+ }
+
+ foreach ((array)$conf['trustedproxies'] as $trusted) {
+ if (Ip::ipMatches($ip, $trusted)) {
+ return true; // The given IP matches one of the trusted proxies.
+ }
+ }
+
+ return false; // none of the proxies matched
+ }
+
+ /**
+ * 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.
+ */
+ public static function forwardedFor(): array
+ {
+ /* @var Input $INPUT */
+ global $INPUT, $conf;
+
+ $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR');
+
+ if (empty($conf['trustedproxies']) || !$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 (!Ip::proxyIsTrusted($proxy)) {
+ return [];
+ }
+ }
+
+ // Add the client address before the list of proxies.
+ return array_merge([$clientAddress], $proxies);
+ }
+
+ /**
+ * Return the IP of the client.
+ *
+ * The IP is sourced from, in order of preference:
+ *
+ * - The X-Real-IP header if $conf[realip] is true.
+ * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
+ * - The TCP/IP connection remote address.
+ * - 0.0.0.0 if all else fails.
+ *
+ * The 'realip' config value should only be set to true if the X-Real-IP header
+ * is being added by the web server, otherwise it may be spoofed by the client.
+ *
+ * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
+ * may be spoofed by the client.
+ *
+ * @return string Returns an IPv4 or IPv6 address.
+ */
+ public static function clientIp(): string
+ {
+ return Ip::clientIps()[0];
+ }
+
+ /**
+ * Return the IP of the client and the proxies through which the connection has passed.
+ *
+ * The IPs are sourced from, in order of preference:
+ *
+ * - The X-Real-IP header if $conf[realip] is true.
+ * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
+ * - The TCP/IP connection remote address.
+ * - 0.0.0.0 if all else fails.
+ *
+ * @return string[] Returns an array of IPv4 or IPv6 addresses.
+ */
+ public static function clientIps(): array
+ {
+ /* @var Input $INPUT */
+ global $INPUT, $conf;
+
+ // IPs in order of most to least preferred.
+ $ips = [];
+
+ // Use the X-Real-IP header if it is enabled by the configuration.
+ if (!empty($conf['realip']) && $INPUT->server->str('HTTP_X_REAL_IP')) {
+ $ips[] = $INPUT->server->str('HTTP_X_REAL_IP');
+ }
+
+ // Add the X-Forwarded-For addresses if all proxies are trusted.
+ $ips = array_merge($ips, Ip::forwardedFor());
+
+ // Add the TCP/IP connection endpoint.
+ $ips[] = $INPUT->server->str('REMOTE_ADDR');
+
+ // Remove invalid IPs.
+ $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP));
+
+ // Remove duplicated IPs.
+ $ips = array_values(array_unique($ips));
+
+ // Add a fallback if for some reason there were no valid IPs.
+ if (!$ips) {
+ $ips[] = '0.0.0.0';
+ }
+
+ return $ips;
+ }
+}
diff --git a/inc/common.php b/inc/common.php
index f0aa5b31f..e674b8097 100644
--- a/inc/common.php
+++ b/inc/common.php
@@ -19,6 +19,7 @@ use dokuwiki\Subscriptions\PageSubscriptionSender;
use dokuwiki\Subscriptions\SubscriberManager;
use dokuwiki\Extension\AuthPlugin;
use dokuwiki\Extension\Event;
+use dokuwiki\Ip;
use function PHP81_BC\strftime;
@@ -778,58 +779,32 @@ function checkwordblock($text = '')
}
/**
- * Return the IP of the client
+ * Return the IP of the client.
*
- * Honours X-Forwarded-For and X-Real-IP Proxy Headers
+ * The IP is sourced from, in order of preference:
*
- * It returns a comma separated list of IPs if the above mentioned
- * headers are set. If the single parameter is set, it tries to return
- * a routable public address, prefering the ones suplied in the X
- * headers
+ * - The X-Real-IP header if $conf[realip] is true.
+ * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
+ * - The TCP/IP connection remote address.
+ * - 0.0.0.0 if all else fails.
*
- * @param boolean $single If set only a single IP is returned
- * @return string
- * @author Andreas Gohr <andi@splitbrain.org>
+ * The 'realip' config value should only be set to true if the X-Real-IP header
+ * is being added by the web server, otherwise it may be spoofed by the client.
+ *
+ * The 'trustedproxies' setting must not allow any IP, otherwise the X-Forwarded-For
+ * may be spoofed by the client.
+ *
+ * @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.
+ * @author Zebra North <mrzebra@mrzebra.co.uk>
*
*/
function clientIP($single = false)
{
- /* @var Input $INPUT */
- global $INPUT, $conf;
-
- $ip = [];
- $ip[] = $INPUT->server->str('REMOTE_ADDR');
- if ($INPUT->server->str('HTTP_X_FORWARDED_FOR')) {
- $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_FORWARDED_FOR'))));
- }
- if ($INPUT->server->str('HTTP_X_REAL_IP')) {
- $ip = array_merge($ip, explode(',', str_replace(' ', '', $INPUT->server->str('HTTP_X_REAL_IP'))));
- }
-
- // remove any non-IP stuff
- $cnt = count($ip);
- for ($i = 0; $i < $cnt; $i++) {
- if (filter_var($ip[$i], FILTER_VALIDATE_IP) === false) {
- unset($ip[$i]);
- }
- }
- $ip = array_values(array_unique($ip));
- if ($ip === [] || !$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP
-
- if (!$single) return implode(',', $ip);
-
- // skip trusted local addresses
- foreach ($ip as $i) {
- if (!empty($conf['trustedproxy']) && preg_match('/' . $conf['trustedproxy'] . '/', $i)) {
- continue;
- } else {
- return $i;
- }
- }
-
- // still here? just use the last address
- // this case all ips in the list are trusted
- return $ip[count($ip) - 1];
+ // Return the first IP in single mode, or all the IPs.
+ return $single ? Ip::clientIp() : join(',', Ip::clientIps());
}
/**
diff --git a/lib/plugins/config/lang/en/lang.php b/lib/plugins/config/lang/en/lang.php
index f1598e2ef..c08ca5b07 100644
--- a/lib/plugins/config/lang/en/lang.php
+++ b/lib/plugins/config/lang/en/lang.php
@@ -188,7 +188,6 @@ $lang['search_fragment_o_exact'] = 'exact';
$lang['search_fragment_o_starts_with'] = 'starts with';
$lang['search_fragment_o_ends_with'] = 'ends with';
$lang['search_fragment_o_contains'] = 'contains';
-$lang['trustedproxy'] = 'Trust forwarding proxies matching this regular expression about the true client IP they report. The default matches local networks. Leave empty to trust no proxy.';
$lang['_feature_flags'] = 'Feature Flags';
$lang['defer_js'] = 'Defer javascript to be execute after the page\'s HTML has been parsed. Improves perceived page speed but could break a small number of plugins.';
@@ -197,6 +196,8 @@ $lang['hidewarnings'] = 'Do not display any warnings issued by PHP. This may eas
/* Network Options */
$lang['dnslookups'] = 'DokuWiki will lookup hostnames for remote IP addresses of users editing pages. If you have a slow or non working DNS server or don\'t want this feature, disable this option';
$lang['jquerycdn'] = 'Should the jQuery and jQuery UI script files be loaded from a CDN? This adds additional HTTP requests, but files may load faster and users may have them cached already.';
+$lang['trustedproxies'] = 'Comma-separated list of trusted proxy servers from which to read the X-Forwarded-For header. Each item in the array may be either an IPv4 or IPv6 address, or an IPv4 or IPv6 CIDR range (e.g. 10.0.0.0/8). Leave empty to trust no proxy.';
+$lang['realip'] = 'Trust the X-Real-IP header. Only enable this if your server writes this header, otherwise it may be spoofed.';
/* jQuery CDN options */
$lang['jquerycdn_o_0'] = 'No CDN, local delivery only';
diff --git a/lib/plugins/config/settings/config.metadata.php b/lib/plugins/config/settings/config.metadata.php
index 2935fb7ff..81da7b827 100644
--- a/lib/plugins/config/settings/config.metadata.php
+++ b/lib/plugins/config/settings/config.metadata.php
@@ -247,7 +247,6 @@ $meta['renderer_xhtml'] = ['renderer', '_format' => 'xhtml', '_choices' => ['xht
$meta['readdircache'] = ['numeric'];
$meta['search_nslimit'] = ['numeric', '_min' => 0];
$meta['search_fragment'] = ['multichoice', '_choices' => ['exact', 'starts_with', 'ends_with', 'contains']];
-$meta['trustedproxy'] = ['regex'];
$meta['_feature_flags'] = ['fieldset'];
$meta['defer_js'] = ['onoff'];
@@ -256,6 +255,8 @@ $meta['hidewarnings'] = ['onoff'];
$meta['_network'] = ['fieldset'];
$meta['dnslookups'] = ['onoff'];
$meta['jquerycdn'] = ['multichoice', '_choices' => [0, 'jquery', 'cdnjs']];
+$meta['trustedproxies'] = ['array', '_caution' => 'security'];
+$meta['realip'] = ['onoff', '_caution' => 'security'];
$meta['proxy____host'] = ['string', '_pattern' => '#^(|[a-z0-9\-\.+]+)$#i'];
$meta['proxy____port'] = ['numericopt'];
$meta['proxy____user'] = ['string'];