3 * This library is used with the server IP allow/deny host authentication
7 declare(strict_types
=1);
14 use function hash_equals
;
16 use function inet_pton
;
18 use function mb_strtolower
;
19 use function mb_substr
;
21 use function preg_match
;
22 use function str_contains
;
23 use function str_replace
;
24 use function substr_replace
;
27 * PhpMyAdmin\IpAllowDeny class
31 private readonly Config
$config;
33 public function __construct()
35 $this->config
= Config
::getInstance();
39 * Matches for IPv4 or IPv6 addresses
41 * @param string $testRange string of IP range to match
42 * @param string $ipToTest string of IP to test against range
44 public function ipMaskTest(string $testRange, string $ipToTest): bool
46 if (str_contains($testRange, ':') ||
str_contains($ipToTest, ':')) {
48 return $this->ipv6MaskTest($testRange, $ipToTest);
51 return $this->ipv4MaskTest($testRange, $ipToTest);
55 * Based on IP Pattern Matcher
56 * Originally by J.Adams <jna@retina.net>
57 * Found on <https://www.php.net/manual/en/function.ip2long.php>
58 * Modified for phpMyAdmin
61 * xxx.xxx.xxx.xxx (exact)
62 * xxx.xxx.xxx.[yyy-zzz] (range)
63 * xxx.xxx.xxx.xxx/nn (CIDR)
66 * xxx.xxx.xxx.xx[yyy-zzz] (range, partial octets not supported)
68 * @param string $testRange string of IP range to match
69 * @param string $ipToTest string of IP to test against range
71 public function ipv4MaskTest(string $testRange, string $ipToTest): bool
74 $match = preg_match('|([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+)|', $testRange, $regs);
76 // performs a mask match
77 $ipl = ip2long($ipToTest);
78 $rangel = ip2long($regs[1] . '.' . $regs[2] . '.' . $regs[3] . '.' . $regs[4]);
82 /** @infection-ignore-all */
83 for ($i = 0; $i < 31; $i++
) {
84 if ($i >= $regs[5] - 1) {
88 $maskl +
= 2 ** (30 - $i);
91 return ($maskl & $rangel) === ($maskl & $ipl);
95 $maskocts = explode('.', $testRange);
96 $ipocts = explode('.', $ipToTest);
98 // perform a range match
99 for ($i = 0; $i < 4; $i++
) {
100 if (preg_match('|\[([0-9]+)\-([0-9]+)\]|', $maskocts[$i], $regs)) {
101 if ($ipocts[$i] > $regs[2] ||
$ipocts[$i] < $regs[1]) {
104 } elseif ($maskocts[$i] !== $ipocts[$i]) {
114 * CIDR section taken from https://stackoverflow.com/a/10086404
115 * Modified for phpMyAdmin
118 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
120 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:[yyyy-zzzz]
121 * (range, only at end of IP - no subnets)
122 * xxxx:xxxx:xxxx:xxxx/nn
126 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xx[yyy-zzz]
127 * (range, partial octets not supported)
129 * @param string $testRange string of IP range to match
130 * @param string $ipToTest string of IP to test against range
132 public function ipv6MaskTest(string $testRange, string $ipToTest): bool
136 // convert to lowercase for easier comparison
137 $testRange = mb_strtolower($testRange);
138 $ipToTest = mb_strtolower($ipToTest);
140 $isCidr = str_contains($testRange, '/');
141 $isRange = str_contains($testRange, '[');
142 $isSingle = ! $isCidr && ! $isRange;
144 $ipHex = bin2hex((string) inet_pton($ipToTest));
147 $rangeHex = bin2hex((string) inet_pton($testRange));
149 return hash_equals($ipHex, $rangeHex);
153 // what range do we operate on?
155 $match = preg_match('/\[([0-9a-f]+)\-([0-9a-f]+)\]/', $testRange, $rangeMatch);
157 $rangeStart = $rangeMatch[1];
158 $rangeEnd = $rangeMatch[2];
160 // get the first and last allowed IPs
161 $firstIp = str_replace($rangeMatch[0], $rangeStart, $testRange);
162 $firstHex = bin2hex((string) inet_pton($firstIp));
163 $lastIp = str_replace($rangeMatch[0], $rangeEnd, $testRange);
164 $lastHex = bin2hex((string) inet_pton($lastIp));
166 // check if the IP to test is within the range
167 $result = $ipHex >= $firstHex && $ipHex <= $lastHex;
174 // Split in address and prefix length
175 [$firstIp, $subnet] = explode('/', $testRange);
177 // Parse the address into a binary string
178 $firstBin = inet_pton($firstIp);
179 $firstHex = bin2hex((string) $firstBin);
181 $flexbits = 128 - (int) $subnet;
183 // Build the hexadecimal string of the last address
184 $lastHex = $firstHex;
187 while ($flexbits > 0) {
188 // Get the character at this position
189 $orig = mb_substr($lastHex, $pos, 1);
191 // Convert it to an integer
192 $origval = hexdec($orig);
194 // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time
195 $newval = $origval |
2 ** min(4, $flexbits) - 1;
197 // Convert it back to a hexadecimal character
198 $new = dechex($newval);
200 // And put that character back in the string
201 $lastHex = substr_replace($lastHex, $new, $pos, 1);
203 // We processed one nibble, move to previous position
204 /** @infection-ignore-all */
209 // check if the IP to test is within the range
210 $result = $ipHex >= $firstHex && $ipHex <= $lastHex;
217 * Runs through IP Allow rules the use of it below for more information
221 public function allow(): bool
223 return $this->allowDeny('allow');
227 * Runs through IP Deny rules the use of it below for more information
231 public function deny(): bool
233 return $this->allowDeny('deny');
237 * Runs through IP Allow/Deny rules the use of it below for more information
241 * @param string $type 'allow' | 'deny' type of rule to match
243 private function allowDeny(string $type): bool
245 // Grabs true IP of the user and returns if it can't be found
246 $remoteIp = Core
::getIp();
247 if ($remoteIp === '' ||
$remoteIp === false) {
252 $username = $this->config
->selectedServer
['user'];
254 // copy rule database
255 $rules = $this->config
->selectedServer
['AllowDeny']['rules'];
257 // lookup table for some name shortcuts
258 $shortcuts = ['all' => '0.0.0.0/0', 'localhost' => '127.0.0.1/8'];
260 // Provide some useful shortcuts if server gives us address:
261 if (Core
::getEnv('SERVER_ADDR') !== '') {
262 $shortcuts['localnetA'] = Core
::getEnv('SERVER_ADDR') . '/8';
263 $shortcuts['localnetB'] = Core
::getEnv('SERVER_ADDR') . '/16';
264 $shortcuts['localnetC'] = Core
::getEnv('SERVER_ADDR') . '/24';
267 foreach ($rules as $rule) {
269 $ruleData = explode(' ', $rule);
271 // check for rule type
272 if ($ruleData[0] !== $type) {
276 // check for username
278 $ruleData[1] !== '%' //wildcarded first
279 && ! hash_equals($ruleData[1], $username)
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 ($ruleData[2] === 'from') {
287 $ruleData[2] = $ruleData[3];
290 // Handle shortcuts with above array
291 if (isset($shortcuts[$ruleData[2]])) {
292 $ruleData[2] = $shortcuts[$ruleData[2]];
295 // Add code for host lookups here
296 // Excluded for the moment
298 // Do the actual matching now
299 if ($this->ipMaskTest($ruleData[2], $remoteIp)) {