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