Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / IpAllowDeny.php
blob2181c2d27e4a0d6e212734af4d701e7143e893f3
1 <?php
2 /**
3 * This library is used with the server IP allow/deny host authentication
4 * feature
5 */
7 declare(strict_types=1);
9 namespace PhpMyAdmin;
11 use function bin2hex;
12 use function dechex;
13 use function explode;
14 use function hash_equals;
15 use function hexdec;
16 use function inet_pton;
17 use function ip2long;
18 use function mb_strtolower;
19 use function mb_substr;
20 use function min;
21 use function preg_match;
22 use function str_contains;
23 use function str_replace;
24 use function substr_replace;
26 /**
27 * PhpMyAdmin\IpAllowDeny class
29 class IpAllowDeny
31 private readonly Config $config;
33 public function __construct()
35 $this->config = Config::getInstance();
38 /**
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, ':')) {
47 // assume IPv6
48 return $this->ipv6MaskTest($testRange, $ipToTest);
51 return $this->ipv4MaskTest($testRange, $ipToTest);
54 /**
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
60 * Matches:
61 * xxx.xxx.xxx.xxx (exact)
62 * xxx.xxx.xxx.[yyy-zzz] (range)
63 * xxx.xxx.xxx.xxx/nn (CIDR)
65 * Does not match:
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
73 $result = true;
74 $match = preg_match('|([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+)|', $testRange, $regs);
75 if ($match) {
76 // performs a mask match
77 $ipl = ip2long($ipToTest);
78 $rangel = ip2long($regs[1] . '.' . $regs[2] . '.' . $regs[3] . '.' . $regs[4]);
80 $maskl = 0;
82 /** @infection-ignore-all */
83 for ($i = 0; $i < 31; $i++) {
84 if ($i >= $regs[5] - 1) {
85 continue;
88 $maskl += 2 ** (30 - $i);
91 return ($maskl & $rangel) === ($maskl & $ipl);
94 // range based
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]) {
102 $result = false;
104 } elseif ($maskocts[$i] !== $ipocts[$i]) {
105 $result = false;
109 return $result;
113 * IPv6 matcher
114 * CIDR section taken from https://stackoverflow.com/a/10086404
115 * Modified for phpMyAdmin
117 * Matches:
118 * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
119 * (exact)
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
123 * (CIDR)
125 * Does not match:
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
134 $result = true;
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));
146 if ($isSingle) {
147 $rangeHex = bin2hex((string) inet_pton($testRange));
149 return hash_equals($ipHex, $rangeHex);
152 if ($isRange) {
153 // what range do we operate on?
154 $rangeMatch = [];
155 $match = preg_match('/\[([0-9a-f]+)\-([0-9a-f]+)\]/', $testRange, $rangeMatch);
156 if ($match) {
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;
170 return $result;
173 if ($isCidr) {
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;
186 $pos = 31;
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 */
205 $flexbits -= 4;
206 --$pos;
209 // check if the IP to test is within the range
210 $result = $ipHex >= $firstHex && $ipHex <= $lastHex;
213 return $result;
217 * Runs through IP Allow rules the use of it below for more information
219 * @see Core::getIp()
221 public function allow(): bool
223 return $this->allowDeny('allow');
227 * Runs through IP Deny rules the use of it below for more information
229 * @see Core::getIp()
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
239 * @see Core::getIp()
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) {
248 return false;
251 // copy username
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) {
268 // extract rule data
269 $ruleData = explode(' ', $rule);
271 // check for rule type
272 if ($ruleData[0] !== $type) {
273 continue;
276 // check for username
277 if (
278 $ruleData[1] !== '%' //wildcarded first
279 && ! hash_equals($ruleData[1], $username)
281 continue;
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)) {
300 return true;
304 return false;