PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-ip / src / class-utils.php

class-utils.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-ip/src/class-utils.php

646 lines 21.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Utils class file.
4 *
5 * @package automattic/jetpack-ip
6 */
7
8 namespace Automattic\Jetpack\IP;
9
10 /**
11 * Class that provides static methods for working with IP addresses.
12 */
13 class Utils {
14
15 const PACKAGE_VERSION = '0.6.0';
16
17 /**
18 * Get the current user's IP address.
19 *
20 * @return string|false IP address.
21 */
22 public static function get_ip() {
23 $trusted_header_data = get_site_option( 'trusted_ip_header' );
24 if ( isset( $trusted_header_data->trusted_header ) && isset( $_SERVER[ $trusted_header_data->trusted_header ] ) ) {
25 $ip = wp_unslash( $_SERVER[ $trusted_header_data->trusted_header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it below.
26 $segments = $trusted_header_data->segments;
27 $reverse_order = $trusted_header_data->reverse;
28 } else {
29 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it below.
30 }
31
32 if ( ! $ip ) {
33 return false;
34 }
35
36 $ips = explode( ',', $ip );
37 if ( ! isset( $segments ) || ! $segments ) {
38 $segments = 1;
39 }
40 if ( isset( $reverse_order ) && $reverse_order ) {
41 $ips = array_reverse( $ips );
42 }
43 $ip_count = count( $ips );
44 if ( 1 === $ip_count ) {
45 return self::clean_ip( $ips[0] );
46 } elseif ( $ip_count >= $segments ) {
47 $the_one = $ip_count - $segments;
48 return self::clean_ip( $ips[ $the_one ] );
49 } else {
50 return self::clean_ip( isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip does it.
51 }
52 }
53
54 /**
55 * Clean IP address.
56 *
57 * @param string $ip The IP address to clean.
58 * @return string|false The cleaned IP address.
59 */
60 public static function clean_ip( $ip ) {
61
62 // Some misconfigured servers give back extra info, which comes after "unless".
63 $ips = explode( ' unless ', $ip );
64 $ip = $ips[0];
65
66 $ip = strtolower( trim( $ip ) );
67
68 // Check for IPv4 with port.
69 if ( preg_match( '/^(\d+\.\d+\.\d+\.\d+):\d+$/', $ip, $matches ) ) {
70 $ip = $matches[1];
71 }
72
73 // Check for IPv6 (or IPvFuture) with brackets and optional port.
74 if ( preg_match( '/^\[([a-z0-9\-._~!$&\'()*+,;=:]+)\](?::\d+)?$/', $ip, $matches ) ) {
75 $ip = $matches[1];
76 }
77
78 // Check for IPv4 IP cast as IPv6.
79 if ( preg_match( '/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $matches ) ) {
80 $ip = $matches[1];
81 }
82
83 // Validate and return.
84 return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : false;
85 }
86
87 /**
88 * Checks an IP to see if it is within a private range.
89 *
90 * @param string $ip IP address.
91 * @return bool True if IP address is private, false otherwise.
92 */
93 public static function ip_is_private( $ip ) {
94 // We are dealing with ipv6, so we can simply rely on filter_var.
95 // Note: str_contains() is not used here, as wp-includes/compat.php may not be loaded in this file.
96 if ( false === strpos( $ip, '.' ) ) {
97 return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
98 }
99 // We are dealing with ipv4.
100 $private_ip4_addresses = array(
101 '10.0.0.0|10.255.255.255', // Single class A network.
102 '172.16.0.0|172.31.255.255', // 16 contiguous class B network.
103 '192.168.0.0|192.168.255.255', // 256 contiguous class C network.
104 '169.254.0.0|169.254.255.255', // Link-local address also referred to as Automatic Private IP Addressing.
105 '127.0.0.0|127.255.255.255', // localhost.
106 );
107 $long_ip = ip2long( $ip );
108 if ( -1 !== $long_ip ) {
109 foreach ( $private_ip4_addresses as $pri_addr ) {
110 list ( $start, $end ) = explode( '|', $pri_addr );
111 if ( $long_ip >= ip2long( $start ) && $long_ip <= ip2long( $end ) ) {
112 return true;
113 }
114 }
115 }
116 return false;
117 }
118
119 /**
120 * Checks whether an IP address is a public, globally-routable destination.
121 *
122 * Stricter than the inverse of ip_is_private(): on top of private and reserved
123 * ranges it also rejects the IPv4 special-use ranges PHP's reserved-range
124 * filter leaves open (CGNAT, IETF protocol assignments, 6to4 relay anycast,
125 * benchmarking, multicast), the Azure metadata "Wire Server" address, and IPv6
126 * link-local / unique-local / site-local ranges. IPv6 addresses that embed an
127 * IPv4 address (IPv4-mapped ::ffff:0:0/96, IPv4-compatible ::/96, NAT64
128 * 64:ff9b::/96, and 6to4 2002::/16) are decoded to that IPv4 and re-checked, so
129 * the embedded IPv4 is classified the same way whether or not it is wrapped.
130 *
131 * @param string $ip IP address (IPv4, IPv6, or IPv4-mapped IPv6; any IPv6 zone id is ignored).
132 * @return bool True when the address is a safe public destination, false otherwise.
133 */
134 public static function ip_is_public( $ip ) {
135 if ( ! is_string( $ip ) || '' === $ip ) {
136 return false;
137 }
138
139 // Strip an IPv6 zone identifier (e.g. fe80::1%eth0). Zone ids are only valid
140 // on IPv6 addresses, so a '%' on anything else (e.g. "8.8.8.8%foo") is malformed.
141 if ( false !== strpos( $ip, '%' ) ) {
142 if ( false === strpos( $ip, ':' ) ) {
143 return false;
144 }
145 $ip = preg_replace( '/%.*$/', '', $ip );
146 }
147
148 // Decode IPv6 forms that embed an IPv4 address to that IPv4 and check it,
149 // so the embedded IPv4 is classified the same way whether or not it is wrapped.
150 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
151 $binary = inet_pton( $ip );
152 if ( false !== $binary && 16 === strlen( $binary ) ) {
153 $prefix12 = substr( $binary, 0, 12 );
154 $embedded = null;
155
156 if (
157 "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" === $prefix12 // IPv4-mapped ::ffff:0:0/96.
158 || "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" === $prefix12 // IPv4-compatible ::/96 (incl. ::, ::1).
159 || "\x00\x64\xff\x9b\x00\x00\x00\x00\x00\x00\x00\x00" === $prefix12 // NAT64 64:ff9b::/96.
160 ) {
161 $embedded = substr( $binary, 12, 4 );
162 } elseif ( "\x20\x02" === substr( $binary, 0, 2 ) ) {
163 // 6to4 2002::/16: the embedded IPv4 gateway is in bytes 2-5.
164 $embedded = substr( $binary, 2, 4 );
165 }
166
167 if ( null !== $embedded ) {
168 $mapped = inet_ntop( $embedded );
169 if ( is_string( $mapped ) ) {
170 $ip = $mapped;
171 }
172 }
173 }
174 }
175
176 // Reject anything that is not a valid, non-private, non-reserved address.
177 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
178 return false;
179 }
180
181 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
182 // IPv4 special-use ranges the reserved-range filter above leaves open.
183 $blocked_ranges = array(
184 array( '100.64.0.0', '100.127.255.255' ), // CGNAT (RFC 6598).
185 array( '168.63.129.16', '168.63.129.16' ), // Azure metadata "Wire Server".
186 array( '169.254.0.0', '169.254.255.255' ), // Link-local, incl. cloud metadata.
187 array( '192.0.0.0', '192.0.0.255' ), // IETF protocol assignments (RFC 6890).
188 array( '192.88.99.0', '192.88.99.255' ), // 6to4 relay anycast (RFC 7526).
189 array( '198.18.0.0', '198.19.255.255' ), // Benchmarking (RFC 2544).
190 array( '224.0.0.0', '239.255.255.255' ), // Multicast (RFC 5771).
191 );
192 foreach ( $blocked_ranges as $range ) {
193 if ( self::ip_address_is_in_range( $ip, $range[0], $range[1] ) ) {
194 return false;
195 }
196 }
197 return true;
198 }
199
200 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
201 $binary = inet_pton( $ip );
202 if ( false === $binary || strlen( $binary ) < 2 ) {
203 return false;
204 }
205 $first = unpack( 'n', $binary )[1];
206
207 // fe80::/10 link-local and fec0::/10 site-local (deprecated).
208 if ( 0xfe80 === ( $first & 0xffc0 ) || 0xfec0 === ( $first & 0xffc0 ) ) {
209 return false;
210 }
211 // fc00::/7 unique local addresses.
212 if ( 0xfc00 === ( $first & 0xfe00 ) ) {
213 return false;
214 }
215 return true;
216 }
217
218 return false;
219 }
220
221 /**
222 * Validate an IP address.
223 *
224 * @param string $ip IP address.
225 * @return bool True if valid, false otherwise.
226 */
227 private static function validate_ip_address( string $ip ) {
228 return filter_var( $ip, FILTER_VALIDATE_IP );
229 }
230
231 /**
232 * Validate an array of IP addresses.
233 *
234 * @param array $ips List of IP addresses.
235 * @return bool True if all IPs are valid, false otherwise.
236 */
237 private static function validate_ip_addresses( array $ips ) {
238 foreach ( $ips as $ip ) {
239 if ( ! self::validate_ip_address( $ip ) ) {
240 return false;
241 }
242 }
243 return true;
244 }
245
246 /**
247 * Uses inet_pton if available to convert an IP address to a binary string.
248 * Returns false if an invalid IP address is given.
249 *
250 * @param mixed $ip IP address.
251 * @return int|string|bool
252 */
253 public static function convert_ip_address( $ip ) {
254 return inet_pton( $ip );
255 }
256
257 /**
258 * Determines the IP version of the given IP address.
259 *
260 * @param string $ip IP address.
261 * @return string|false 'ipv4', 'ipv6', or false if invalid.
262 */
263 public static function get_ip_version( $ip ) {
264 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
265 return 'ipv4';
266 } elseif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
267 return 'ipv6';
268 } else {
269 return false;
270 }
271 }
272
273 /**
274 * Extracts IP addresses from a given string.
275 *
276 * Supports IPv4 and IPv6 ranges in both hyphen and CIDR notation.
277 *
278 * @param string $ips List of IPs.
279 * @return array List of valid IP addresses or ranges.
280 */
281 public static function get_ip_addresses_from_string( $ips ) {
282 // Split the string by spaces, commas, and semicolons.
283 $ips = preg_split( '/[\s,;]/', (string) $ips );
284
285 $result = array();
286
287 foreach ( $ips as $ip ) {
288 $ip = trim( $ip );
289
290 // Check for CIDR notation
291 if ( strpos( $ip, '/' ) !== false ) {
292 if ( self::validate_cidr( $ip ) ) {
293 $result[] = $ip;
294 }
295 continue;
296 }
297
298 // Validate both IP values from the hyphen range.
299 $range = explode( '-', $ip );
300 if ( count( $range ) === 2 ) {
301 if ( self::validate_ip_range( $range[0], $range[1] ) ) {
302 $result[] = $ip;
303 }
304 continue;
305 }
306
307 // Validate the single IP value.
308 if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
309 $result[] = $ip;
310 }
311 }
312
313 return $result;
314 }
315
316 /**
317 * Validates CIDR notation for IPv4 and IPv6 addresses.
318 *
319 * @param string $cidr CIDR notation IP address.
320 * @return bool True if valid, false otherwise.
321 */
322 public static function validate_cidr( $cidr ) {
323 // Split the CIDR notation into IP address and prefix length using the '/' separator.
324 $parts = explode( '/', $cidr );
325 if ( count( $parts ) !== 2 ) {
326 return false; // Invalid CIDR notation if it doesn't contain exactly one '/'.
327 }
328
329 list( $ip, $netmask ) = $parts;
330
331 // Validate the IP address.
332 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
333 return false;
334 }
335
336 $ip_version = self::get_ip_version( $ip );
337 if ( ! $ip_version ) {
338 return false; // Invalid IP address.
339 }
340
341 // Validate the netmask based on the IP version.
342 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
343 return false;
344 }
345
346 return true;
347 }
348
349 /**
350 * Checks if an IP address is within a CIDR range.
351 * Supports both IPv4 and IPv6.
352 *
353 * @param string $ip IP address.
354 * @param string $cidr CIDR notation IP range.
355 * @return bool True if IP is within the range, false otherwise.
356 */
357 public static function ip_in_cidr( $ip, $cidr ) {
358 // Parse the CIDR notation to extract the base IP address and netmask prefix length.
359 $parsed_cidr = self::parse_cidr( $cidr );
360 if ( ! $parsed_cidr ) {
361 return false;
362 }
363 list( $range, $netmask ) = $parsed_cidr;
364
365 // Determine the IP version (IPv4 or IPv6) of both the input IP and the CIDR range IP.
366 $ip_version = self::get_ip_version( $ip );
367 $range_version = self::get_ip_version( $range );
368
369 // Ensure both IP addresses are valid and of the same IP version.
370 if ( ! $ip_version || ! $range_version || $ip_version !== $range_version ) {
371 return false;
372 }
373
374 // Validate the netmask based on the IP version.
375 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
376 return false;
377 }
378
379 if ( $ip_version === 'ipv4' ) {
380 return self::ip_in_ipv4_cidr( $ip, $range, $netmask );
381 } else {
382 return self::ip_in_ipv6_cidr( $ip, $range, $netmask );
383 }
384 }
385
386 /**
387 * Parses the CIDR notation into network address and netmask.
388 *
389 * @param string $cidr CIDR notation IP range.
390 * @return array|false Array containing network address and netmask, or false on failure.
391 */
392 public static function parse_cidr( $cidr ) {
393 $cidr_parts = explode( '/', $cidr, 2 );
394 if ( count( $cidr_parts ) !== 2 ) {
395 return false; // Invalid CIDR notation
396 }
397 list( $range, $netmask ) = $cidr_parts;
398
399 // Determine IP version
400 $ip_version = self::get_ip_version( $range );
401 if ( ! $ip_version ) {
402 return false; // Invalid IP address
403 }
404
405 // Validate netmask range
406 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
407 return false; // Netmask out of range
408 }
409
410 return array( $range, (int) $netmask );
411 }
412
413 /**
414 * Validates the netmask based on IP version.
415 *
416 * @param string|int $netmask Netmask value.
417 * @param string $ip_version 'ipv4' or 'ipv6'.
418 * @return bool True if valid, false otherwise.
419 */
420 public static function validate_netmask( $netmask, $ip_version ) {
421 // Ensure that $netmask is an integer
422 if ( ! ctype_digit( (string) $netmask ) ) {
423 return false;
424 }
425 $netmask = (int) $netmask;
426
427 // Validate the netmask based on the IP version.
428 if ( $ip_version === 'ipv4' ) {
429 return ( $netmask >= 0 && $netmask <= 32 );
430 } elseif ( $ip_version === 'ipv6' ) {
431 return ( $netmask >= 0 && $netmask <= 128 );
432 } else {
433 return false;
434 }
435 }
436
437 /**
438 * Checks if an IPv4 address is within a CIDR range.
439 *
440 * @param string $ip IPv4 address to check.
441 * @param string $range IPv4 network address.
442 * @param int $netmask Netmask value.
443 * @return bool True if IP is within the range, false otherwise.
444 */
445 public static function ip_in_ipv4_cidr( $ip, $range, $netmask ) {
446 // Validate arguments.
447 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv4' ) ) {
448 return false; // Invalid IP address or netmask.
449 }
450
451 // Convert IP addresses from their dotted representation to 32-bit unsigned integers.
452 $ip_long = ip2long( $ip );
453 $range_long = ip2long( $range );
454
455 // Check if the conversion was successful.
456 if ( $ip_long === false || $range_long === false ) {
457 return false; // One of the IP addresses is invalid.
458 }
459
460 /**
461 * Create the subnet mask as a 32-bit unsigned integer.
462 *
463 * Explanation:
464 * - (32 - $netmask) calculates the number of host bits (the bits not used for the network address).
465 * - (1 << (32 - $netmask)) shifts the number 1 left by the number of host bits.
466 * This results in a number where there is a single 1 followed by zeros equal to the number of host bits.
467 * - Subtracting 1 gives us a number where the host bits are all 1s.
468 * - Applying the bitwise NOT operator (~) inverts the bits, turning all host bits to 0 and network bits to 1.
469 * This results in the subnet mask having 1s in the network portion and 0s in the host portion.
470 *
471 * Example for netmask = 24:
472 * - (32 - 24) = 8
473 * - (1 << 8) = 256 (binary: 00000000 00000000 00000001 00000000)
474 * - 256 - 1 = 255 (binary: 00000000 00000000 00000000 11111111)
475 * - ~255 = 4294967040 (binary: 11111111 11111111 11111111 00000000)
476 */
477 $mask = ~ ( ( 1 << ( 32 - $netmask ) ) - 1 );
478
479 /**
480 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
481 * - ($ip_long & $mask) isolates the network portion of the IP address.
482 * - ($range_long & $mask) isolates the network portion of the CIDR range.
483 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
484 */
485 return ( $ip_long & $mask ) === ( $range_long & $mask );
486 }
487
488 /**
489 * Checks if an IPv6 address is within a CIDR range.
490 *
491 * @param string $ip IPv6 address to check.
492 * @param string $range IPv6 network address.
493 * @param int $netmask Netmask value.
494 * @return bool True if IP is within the range, false otherwise.
495 */
496 public static function ip_in_ipv6_cidr( $ip, $range, $netmask ) {
497 // Validate arguments.
498 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv6' ) ) {
499 return false; // Invalid IP address or netmask.
500 }
501
502 // Convert IP addresses from their textual representation to binary strings.
503 $ip_bin = inet_pton( $ip );
504 $range_bin = inet_pton( $range );
505
506 // Check if the conversion was successful.
507 if ( $ip_bin === false || $range_bin === false ) {
508 return false; // One of the IP addresses is invalid.
509 }
510
511 /**
512 * Calculate the subnet mask in binary form.
513 *
514 * IPv6 addresses are 128 bits long.
515 * The netmask defines how many bits are set to 1 in the subnet mask.
516 *
517 * - $netmask_full_bytes: Number of full bytes (each 8 bits) that are all 1s.
518 * - $netmask_remainder_bits: Remaining bits (less than 8) that need to be set to 1.
519 *
520 * For example, if $netmask = 65:
521 * - $netmask_full_bytes = floor(65 / 8) = 8 (since 8 * 8 = 64 bits)
522 * - $netmask_remainder_bits = 65 % 8 = 1 (1 bit remaining)
523 *
524 * We'll construct the subnet mask by:
525 * - Starting with $netmask_full_bytes of 0xff (11111111 in binary).
526 * - Adding a byte where the first $netmask_remainder_bits bits are 1, rest are 0.
527 * - Padding the rest with zeros to make it 16 bytes (128 bits) long.
528 */
529
530 // Number of full bytes (each full byte is 8 bits) in the netmask.
531 $netmask_full_bytes = (int) ( $netmask / 8 );
532
533 // Number of remaining bits in the last byte of the netmask.
534 $netmask_remainder_bits = $netmask % 8;
535
536 // Start with a string of $netmask_full_bytes of 0xff bytes (each byte is 8 bits set to 1).
537 $netmask_bin = str_repeat( "\xff", $netmask_full_bytes );
538
539 if ( $netmask_remainder_bits > 0 ) {
540 // Create the last byte with $netmask_remainder_bits bits set to 1 from the left.
541 // - str_repeat('1', $netmask_remainder_bits): creates a string with the required number of '1's.
542 // - str_pad(...): pads the string on the right with '0's to make it 8 bits.
543 // - bindec(...): converts the binary string to a decimal number.
544 // - chr(...): gets the character corresponding to the byte value.
545 $last_byte = chr( bindec( str_pad( str_repeat( '1', $netmask_remainder_bits ), 8, '0', STR_PAD_RIGHT ) ) );
546 // Append the last byte to the netmask binary string.
547 $netmask_bin .= $last_byte;
548 }
549
550 // Pad the netmask binary string to 16 bytes (128 bits) with zeros (\x00).
551 $netmask_bin = str_pad( $netmask_bin, 16, "\x00" );
552
553 /**
554 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
555 * - ($ip_bin & $netmask_bin) isolates the network portion of the IP address.
556 * - ($range_bin & $netmask_bin) isolates the network portion of the CIDR range.
557 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
558 */
559 return ( $ip_bin & $netmask_bin ) === ( $range_bin & $netmask_bin );
560 }
561
562 /**
563 * Validates the low and high IP addresses of a range.
564 *
565 * Now supports IPv6 addresses.
566 *
567 * @param string $range_low Low IP address.
568 * @param string $range_high High IP address.
569 * @return bool True if the range is valid, false otherwise.
570 */
571 public static function validate_ip_range( $range_low, $range_high ) {
572 // Validate that both IP addresses are valid.
573 if ( self::validate_ip_addresses( array( $range_low, $range_high ) ) === false ) {
574 return false;
575 }
576
577 // Ensure both IPs are of the same version
578 $range_low_ip_version = self::get_ip_version( $range_low );
579 $range_high_ip_version = self::get_ip_version( $range_high );
580
581 if ( $range_low_ip_version !== $range_high_ip_version || ! $range_low_ip_version || ! $range_high_ip_version ) {
582 return false; // Invalid or mixed IP versions.
583 }
584
585 // Convert IP addresses to their packed binary representation.
586 $ip_low = inet_pton( $range_low );
587 $ip_high = inet_pton( $range_high );
588
589 // Check if the conversion was successful.
590 if ( false === $ip_low || false === $ip_high ) {
591 return false;
592 }
593
594 // Compare the binary representations to ensure the low IP is not greater than the high IP.
595 if ( strcmp( $ip_low, $ip_high ) > 0 ) {
596 return false;
597 }
598
599 return true;
600 }
601
602 /**
603 * Checks that a given IP address is within a given range.
604 *
605 * Supports CIDR notation and hyphenated ranges for both IPv4 and IPv6.
606 *
607 * @param string $ip IP address.
608 * @param string $range_low Range low or CIDR notation.
609 * @param null|string $range_high Optional. Range high. Not used if $range_low is CIDR notation.
610 * @return bool
611 */
612 public static function ip_address_is_in_range( $ip, $range_low, $range_high = null ) {
613 // Validate that all provided IP addresses are valid.
614 if ( $range_high !== null && ! self::validate_ip_addresses( array( $ip, $range_low, $range_high ) ) ) {
615 return false;
616 } else {
617 $range_low_parsed = self::parse_cidr( $range_low );
618 if ( $range_low_parsed && ! self::validate_ip_addresses( array( $ip, $range_low_parsed[0] ) ) ) {
619 return false;
620 }
621 }
622
623 if ( strpos( $range_low, '/' ) !== false ) {
624 // CIDR notation
625 if ( $range_high !== null ) {
626 // Invalid usage: CIDR notation with range high parameter
627 return false;
628 }
629 return self::ip_in_cidr( $ip, $range_low );
630 }
631
632 // Hyphenated range
633 if ( $range_high === null ) {
634 return false; // Invalid parameters
635 }
636
637 $ip_num = inet_pton( $ip );
638 $ip_low = inet_pton( $range_low );
639 $ip_high = inet_pton( $range_high );
640 if ( $ip_num && $ip_low && $ip_high && strcmp( $ip_num, $ip_low ) >= 0 && strcmp( $ip_num, $ip_high ) <= 0 ) {
641 return true;
642 }
643 return false;
644 }
645 }
646