diff --git a/admin/settings/server.php b/admin/settings/server.php index c4a0e3e6b1159..a2bf41d5c6854 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -181,8 +181,15 @@ new lang_string('configproxyuser', 'admin'), '')); $temp->add(new admin_setting_configpasswordunmask('proxypassword', new lang_string('proxypassword', 'admin'), new lang_string('configproxypassword', 'admin'), '')); - $temp->add(new admin_setting_configtext('proxybypass', new lang_string('proxybypass', 'admin'), - new lang_string('configproxybypass', 'admin'), 'localhost, 127.0.0.1')); + + $setting = new admin_setting_configtext('proxybypass', new lang_string('proxybypass', 'admin'), + new lang_string('configproxybypass', 'admin'), 'localhost,127.0.0.1'); + $setting->set_updatedcallback(function() { + // Normalize $CFG->proxybypass value. + set_config('proxybypass', \core\ip_utils::normalize_internet_address_list(get_config('core', 'proxybypass'))); + }); + $temp->add($setting); + $temp->add(new admin_setting_configcheckbox('proxylogunsafe', new lang_string('proxylogunsafe', 'admin'), new lang_string('configproxylogunsafe_help', 'admin'), 0)); $temp->add(new admin_setting_configcheckbox('proxyfixunsafe', new lang_string('proxyfixunsafe', 'admin'), diff --git a/lang/en/admin.php b/lang/en/admin.php index 2edff4d2ce9b2..d8e20d43e48ac 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -341,7 +341,7 @@ $string['configprofileroles'] = 'Roles that are listed in user profiles and on the participants page.'; $string['configprofilesforenrolledusersonly'] = 'To prevent misuse by spammers, profile descriptions of users who are not yet enrolled in any course are hidden. New users must enrol in at least one course before they can add a profile description.'; $string['configprotectusernames'] = 'If enabled, the forgotten password form will not display any hints allowing account usernames or email addresses to be guessed.'; -$string['configproxybypass'] = 'Comma separated list of (partial) hostnames or IPs that should bypass proxy (e.g., 192.168., .mydomain.com)'; +$string['configproxybypass'] = 'List of (partial) hostnames or IP addresses that should bypass the proxy. Separate each item by commas, with no spaces. For example: 192.168.0.0/16,*.mydomain.com.'; $string['configproxyhost'] = 'If this server needs to use a proxy computer (eg a firewall) to access the Internet, then provide the proxy hostname here. Otherwise leave it blank.'; $string['configproxypassword'] = 'Password needed to access internet through proxy if required, empty if none (PHP cURL extension required).'; $string['configproxyport'] = 'If this server needs to use a proxy computer, then provide the proxy port here.'; diff --git a/lib/classes/ip_utils.php b/lib/classes/ip_utils.php index 33f56b4387bcf..13e2b0f7062d2 100644 --- a/lib/classes/ip_utils.php +++ b/lib/classes/ip_utils.php @@ -263,4 +263,93 @@ public static function get_ip_address(string $hostname): ?string { return null; } + + /** + * Normalize internet address. + * + * Accepted input formats are : + * - a valid range or full ip address (e.g.: 192.168.0.0/16, fe80::ffff, 127.0.0.1 or fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80) + * - a valid domain name or pattern (e.g.: www.moodle.com or *.moodle.org) + * + * Convert forbidden syntaxes since MDL-74289 to allowed values. For examples: + * - 192.168. => 192.168.0.0/16 + * - .domain.tld => *.domain.tld + * + * @param string $address The input string to normalize. + * + * @return string If $address is not normalizable, an empty string is returned. + */ + public static function normalize_internet_address(string $address): string { + $address = str_replace([" ", "\n", "\r", "\t", "\v", "\x00"], '', strtolower($address)); + + // Replace previous allowed "192.168." format to CIDR format (192.168.0.0/16). + if (str_ends_with($address, '.') && preg_match('/^[0-9\.]+$/', $address) === 1) { + $count = substr_count($address, '.'); + + // Remove final dot. + $address = substr($address, 0, -1); + + // Fill address with missing ".0". + $address .= str_repeat('.0', 4 - $count); + + // Add subnet mask. + $address .= '/' . ($count * 8); + } + + if (ip_utils::is_ip_address($address) || + ip_utils::is_ipv4_range($address) || ip_utils::is_ipv6_range($address)) { + + // Keep full or range ip addresses. + return $address; + } + + // Replace previous allowed ".domain.tld" format to "*.domain.tld" format. + if (str_starts_with($address, '.')) { + $address = '*'.$address; + } + + // Usually the trailing dot (null label) is omitted, but is valid if supplied. We'll just remove it and validate as normal. + $address = rtrim($address, '.'); + + if (ip_utils::is_domain_name($address) || ip_utils::is_domain_matching_pattern($address)) { + // Keep valid or pattern domain name. + return $address; + } + + // Return empty string for invalid values. + return ''; + } + + /** + * Normalize a list of internet addresses. + * + * This function will: + * - normalize internet addresses {@see normalize_internet_address()} + * - remove invalid values + * - remove duplicate values + * + * @param string $addresslist A string representing a list of internet addresses separated by a common value. + * + * @return string + */ + public static function normalize_internet_address_list(string $addresslist, string $separator = ','): string { + $addresses = []; + foreach (explode($separator, $addresslist) as $value) { + $address = self::normalize_internet_address($value); + + if (empty($address)) { + // Ignore invalid input. + continue; + } + + if (in_array($address, $addresses, true)) { + // Ignore duplicate value. + continue; + } + + $addresses[] = $address; + } + + return implode($separator, $addresses); + } } diff --git a/lib/tests/ip_utils_test.php b/lib/tests/ip_utils_test.php index d3fceee8c91b8..19aff1256d3c1 100644 --- a/lib/tests/ip_utils_test.php +++ b/lib/tests/ip_utils_test.php @@ -400,4 +400,155 @@ public function test_is_ip_in_subnet_list($expected, $ip, $list, $delim): void { $this->assertEquals($expected, \core\ip_utils::is_ip_in_subnet_list($ip, $list, $delim)); } + /** + * Data provider for test_normalize_internet_address. + * + * @return array + */ + public function normalize_internet_address_provider(): array { + return [ + 'Strip all white spaces on IP address' => [ + ' 192.168.5.5 ', + '192.168.5.5', + ], + 'Strip all white spaces on domain name' => [ + ' www.moodle.org ', + 'www.moodle.org', + ], + 'Preserve IPv4 address' => [ + '127.0.0.1', + '127.0.0.1', + ], + 'Preserve IPv4 address range' => [ + '192.168.0.0/16', + '192.168.0.0/16', + ], + 'Preserve IPv6 address' => [ + 'fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80', + 'fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80', + ], + 'Preserve IPv6 address range' => [ + 'fe80::ffff', + 'fe80::ffff', + ], + 'Preserve valid domain' => [ + 'localhost', + 'localhost', + ], + 'Preserve valid FQDN' => [ + 'www.moodle.org', + 'www.moodle.org', + ], + 'Preserve valid FQDN with trailing dot' => [ + 'www.moodle.com.', + 'www.moodle.com', + ], + 'Preserve valid domain with wildcard' => [ + '*.moodledev.io', + '*.moodledev.io', + ], + 'Convert previous allowed "127." format to CIDR format (127.0.0.0/8)' => [ + '127.', + '127.0.0.0/8', + ], + 'Convert previous allowed "169.8." format to CIDR format (169.8.0.0/16)' => [ + '169.8.', + '169.8.0.0/16', + ], + 'Convert previous allowed "192.168.10." format to CIDR format (192.168.10.0/24)' => [ + '192.168.10.', + '192.168.10.0/24', + ], + 'Convert previous allowed ".moodle.org" subdomain format to new format (*.moodle.org)' => [ + '.moodle.org', + '*.moodle.org', + ], + 'Ignore invalid IPv4' => [ + '327.0.0.1', + '', + ], + 'Ignore invalid IPv4 range' => [ + '192.168', + '', + ], + 'Ignore invalid IPv6' => [ + 'fe80::ddddd', + '', + ], + 'Ignore invalid IPv6 range' => [ + 'fe80:', + '', + ], + 'Ignore invalid domain' => [ + '-example.com', + '', + ], + ]; + } + + /** + * Test if input address value is correctly normalized. + * + * @covers ::normalize_internet_address + * + * @dataProvider normalize_internet_address_provider + * + * @param string $input Raw input value. + * @param string $expected Expected value after normalization. + */ + public function test_normalize_internet_address(string $input, string $expected): void { + $this->assertEquals($expected, \core\ip_utils::normalize_internet_address($input)); + } + + /** + * Data provider for test_normalize_internet_address_list. + * + * @return array + */ + public function normalize_internet_address_list_provider(): array { + return [ + 'Strip all white spaces' => [ + ' 192.168.5.5, 127.0.0.1, www.moodle.org ', + '192.168.5.5,127.0.0.1,www.moodle.org', + ], + 'Trim input' => [ + ' 192.168.5.5,127.0.0.1,www.moodle.org ', + '192.168.5.5,127.0.0.1,www.moodle.org', + ], + 'Preserve valid full and partial IP' => [ + '127.0.0.1,192.168.0.0/16,fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80,fe80::ffff', + '127.0.0.1,192.168.0.0/16,fe80:fe80:fe80:fe80:fe80:fe80:fe80:fe80,fe80::ffff', + ], + 'Convert previous allowed format to new allowed format' => [ + '127.,169.8.,192.168.10.,.moodle.org', + '127.0.0.0/8,169.8.0.0/16,192.168.10.0/24,*.moodle.org', + ], + 'Preserve valid domain and pattern domain' => [ + 'localhost,www.moodle.org,.moodle.com,*.moodledev.io', + 'localhost,www.moodle.org,*.moodle.com,*.moodledev.io', + ], + 'Remove all invalid IP and domains' => [ + '327.0.0.1,192.168,fe80::ddddd,fe80:,-example.com', + '', + ], + 'Remove duplicate values' => [ + '.moodle.org,*.moodle.org,*.moodle.org,.moodle.org', + '*.moodle.org', + ], + ]; + } + + /** + * Test if input address list is correctly normalized. + * + * @covers ::normalize_internet_address_list + * + * @dataProvider normalize_internet_address_list_provider + * + * @param string $input Raw input value. + * @param string $expected Expected value after normalization. + */ + public function test_normalize_internet_address_list(string $input, string $expected): void { + $this->assertEquals($expected, \core\ip_utils::normalize_internet_address_list($input)); + } }