1<?php 2/* vim: set expandtab sw=4 ts=4 sts=4: */ 3/** 4 * This library is used with the server IP allow/deny host authentication 5 * feature 6 * 7 * @package PhpMyAdmin 8 */ 9namespace PhpMyAdmin; 10 11use PhpMyAdmin\Core; 12 13require_once './libraries/hash.lib.php'; 14 15/** 16 * PhpMyAdmin\IpAllowDeny class 17 * 18 * @package PhpMyAdmin 19 */ 20class IpAllowDeny 21{ 22 /** 23 * Matches for IPv4 or IPv6 addresses 24 * 25 * @param string $testRange string of IP range to match 26 * @param string $ipToTest string of IP to test against range 27 * 28 * @return boolean whether the IP mask matches 29 * 30 * @access public 31 */ 32 public static function ipMaskTest($testRange, $ipToTest) 33 { 34 if (mb_strpos($testRange, ':') > -1 35 || mb_strpos($ipToTest, ':') > -1 36 ) { 37 // assume IPv6 38 $result = self::ipv6MaskTest($testRange, $ipToTest); 39 } else { 40 $result = self::ipv4MaskTest($testRange, $ipToTest); 41 } 42 43 return $result; 44 } // end of the "self::ipMaskTest()" function 45 46 /** 47 * Based on IP Pattern Matcher 48 * Originally by J.Adams <jna@retina.net> 49 * Found on <https://www.php.net/manual/en/function.ip2long.php> 50 * Modified for phpMyAdmin 51 * 52 * Matches: 53 * xxx.xxx.xxx.xxx (exact) 54 * xxx.xxx.xxx.[yyy-zzz] (range) 55 * xxx.xxx.xxx.xxx/nn (CIDR) 56 * 57 * Does not match: 58 * xxx.xxx.xxx.xx[yyy-zzz] (range, partial octets not supported) 59 * 60 * @param string $testRange string of IP range to match 61 * @param string $ipToTest string of IP to test against range 62 * 63 * @return boolean whether the IP mask matches 64 * 65 * @access public 66 */ 67 public static function ipv4MaskTest($testRange, $ipToTest) 68 { 69 $result = true; 70 $match = preg_match( 71 '|([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+)|', 72 $testRange, 73 $regs 74 ); 75 if ($match) { 76 // performs a mask match 77 $ipl = ip2long($ipToTest); 78 $rangel = ip2long( 79 $regs[1] . '.' . $regs[2] . '.' . $regs[3] . '.' . $regs[4] 80 ); 81 82 $maskl = 0; 83 84 for ($i = 0; $i < 31; $i++) { 85 if ($i < $regs[5] - 1) { 86 $maskl = $maskl + pow(2, (30 - $i)); 87 } // end if 88 } // end for 89 90 return ($maskl & $rangel) == ($maskl & $ipl); 91 } 92 93 // range based 94 $maskocts = explode('.', $testRange); 95 $ipocts = explode('.', $ipToTest); 96 97 // perform a range match 98 for ($i = 0; $i < 4; $i++) { 99 if (preg_match('|\[([0-9]+)\-([0-9]+)\]|', $maskocts[$i], $regs)) { 100 if (($ipocts[$i] > $regs[2]) || ($ipocts[$i] < $regs[1])) { 101 $result = false; 102 } // end if 103 } else { 104 if ($maskocts[$i] <> $ipocts[$i]) { 105 $result = false; 106 } // end if 107 } // end if/else 108 } //end for 109 110 return $result; 111 } // end of the "self::ipv4MaskTest()" function 112 113 /** 114 * IPv6 matcher 115 * CIDR section taken from https://stackoverflow.com/a/10086404 116 * Modified for phpMyAdmin 117 * 118 * Matches: 119 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx 120 * (exact) 121 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:[yyyy-zzzz] 122 * (range, only at end of IP - no subnets) 123 * xxxx:xxxx:xxxx:xxxx/nn 124 * (CIDR) 125 * 126 * Does not match: 127 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xx[yyy-zzz] 128 * (range, partial octets not supported) 129 * 130 * @param string $test_range string of IP range to match 131 * @param string $ip_to_test string of IP to test against range 132 * 133 * @return boolean whether the IP mask matches 134 * 135 * @access public 136 */ 137 public static function ipv6MaskTest($test_range, $ip_to_test) 138 { 139 $result = true; 140 141 // convert to lowercase for easier comparison 142 $test_range = mb_strtolower($test_range); 143 $ip_to_test = mb_strtolower($ip_to_test); 144 145 $is_cidr = mb_strpos($test_range, '/') > -1; 146 $is_range = mb_strpos($test_range, '[') > -1; 147 $is_single = ! $is_cidr && ! $is_range; 148 149 $ip_hex = bin2hex(inet_pton($ip_to_test)); 150 151 if ($is_single) { 152 $range_hex = bin2hex(inet_pton($test_range)); 153 $result = hash_equals($ip_hex, $range_hex); 154 return $result; 155 } 156 157 if ($is_range) { 158 // what range do we operate on? 159 $range_match = array(); 160 $match = preg_match( 161 '/\[([0-9a-f]+)\-([0-9a-f]+)\]/', $test_range, $range_match 162 ); 163 if ($match) { 164 $range_start = $range_match[1]; 165 $range_end = $range_match[2]; 166 167 // get the first and last allowed IPs 168 $first_ip = str_replace($range_match[0], $range_start, $test_range); 169 $first_hex = bin2hex(inet_pton($first_ip)); 170 $last_ip = str_replace($range_match[0], $range_end, $test_range); 171 $last_hex = bin2hex(inet_pton($last_ip)); 172 173 // check if the IP to test is within the range 174 $result = ($ip_hex >= $first_hex && $ip_hex <= $last_hex); 175 } 176 return $result; 177 } 178 179 if ($is_cidr) { 180 // Split in address and prefix length 181 list($first_ip, $subnet) = explode('/', $test_range); 182 183 // Parse the address into a binary string 184 $first_bin = inet_pton($first_ip); 185 $first_hex = bin2hex($first_bin); 186 187 $flexbits = 128 - $subnet; 188 189 // Build the hexadecimal string of the last address 190 $last_hex = $first_hex; 191 192 $pos = 31; 193 while ($flexbits > 0) { 194 // Get the character at this position 195 $orig = mb_substr($last_hex, $pos, 1); 196 197 // Convert it to an integer 198 $origval = hexdec($orig); 199 200 // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time 201 $newval = $origval | (pow(2, min(4, $flexbits)) - 1); 202 203 // Convert it back to a hexadecimal character 204 $new = dechex($newval); 205 206 // And put that character back in the string 207 $last_hex = substr_replace($last_hex, $new, $pos, 1); 208 209 // We processed one nibble, move to previous position 210 $flexbits -= 4; 211 --$pos; 212 } 213 214 // check if the IP to test is within the range 215 $result = ($ip_hex >= $first_hex && $ip_hex <= $last_hex); 216 } 217 218 return $result; 219 } // end of the "self::ipv6MaskTest()" function 220 221 /** 222 * Runs through IP Allow/Deny rules the use of it below for more information 223 * 224 * @param string $type 'allow' | 'deny' type of rule to match 225 * 226 * @return bool Whether rule has matched 227 * 228 * @access public 229 * 230 * @see Core::getIp() 231 */ 232 public static function allowDeny($type) 233 { 234 global $cfg; 235 236 // Grabs true IP of the user and returns if it can't be found 237 $remote_ip = Core::getIp(); 238 if (empty($remote_ip)) { 239 return false; 240 } 241 242 // copy username 243 $username = $cfg['Server']['user']; 244 245 // copy rule database 246 if (isset($cfg['Server']['AllowDeny']['rules'])) { 247 $rules = $cfg['Server']['AllowDeny']['rules']; 248 if (! is_array($rules)) { 249 $rules = array(); 250 } 251 } else { 252 $rules = array(); 253 } 254 255 // lookup table for some name shortcuts 256 $shortcuts = array( 257 'all' => '0.0.0.0/0', 258 'localhost' => '127.0.0.1/8' 259 ); 260 261 // Provide some useful shortcuts if server gives us address: 262 if (Core::getenv('SERVER_ADDR')) { 263 $shortcuts['localnetA'] = Core::getenv('SERVER_ADDR') . '/8'; 264 $shortcuts['localnetB'] = Core::getenv('SERVER_ADDR') . '/16'; 265 $shortcuts['localnetC'] = Core::getenv('SERVER_ADDR') . '/24'; 266 } 267 268 foreach ($rules as $rule) { 269 // extract rule data 270 $rule_data = explode(' ', $rule); 271 272 // check for rule type 273 if ($rule_data[0] != $type) { 274 continue; 275 } 276 277 // check for username 278 if (($rule_data[1] != '%') //wildcarded first 279 && (! hash_equals($rule_data[1], $username)) 280 ) { 281 continue; 282 } 283 284 // check if the config file has the full string with an extra 285 // 'from' in it and if it does, just discard it 286 if ($rule_data[2] == 'from') { 287 $rule_data[2] = $rule_data[3]; 288 } 289 290 // Handle shortcuts with above array 291 if (isset($shortcuts[$rule_data[2]])) { 292 $rule_data[2] = $shortcuts[$rule_data[2]]; 293 } 294 295 // Add code for host lookups here 296 // Excluded for the moment 297 298 // Do the actual matching now 299 if (self::ipMaskTest($rule_data[2], $remote_ip)) { 300 return true; 301 } 302 } // end while 303 304 return false; 305 } // end of the "self::allowDeny()" function 306} 307