PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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.3-a.7, at jetpack_vendor/automattic/jetpack-ip/src/class-utils.php

768 lines 24.9 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.7.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 * Checks whether a URL is a safe destination for a server-side request.
223 *
224 * The URL must pass wp_http_validate_url(), which rejects IPv6-literal hosts, and every
225 * address its host resolves to must pass ip_is_public(). A host that resolves to none fails.
226 * Not covered: redirect hops (check each one), DNS rebinding, or AAAA records without ext-dns.
227 *
228 * @since 0.7.0
229 *
230 * @param string $url URL to check.
231 * @return bool True when the URL is safe to request, false otherwise.
232 */
233 public static function url_is_public( $url ) {
234 if ( ! is_string( $url ) || '' === $url ) {
235 return false;
236 }
237
238 $validated_url = wp_http_validate_url( $url );
239 if ( ! $validated_url ) {
240 return false;
241 }
242
243 $host = wp_parse_url( $validated_url, PHP_URL_HOST );
244 if ( ! is_string( $host ) || '' === $host ) {
245 return false;
246 }
247
248 $ips = self::resolve_host_ips( $host );
249
250 // Fail closed: an unresolvable host is not assumed safe.
251 if ( empty( $ips ) ) {
252 return false;
253 }
254
255 foreach ( $ips as $ip ) {
256 if ( ! self::ip_is_public( $ip ) ) {
257 return false;
258 }
259 }
260
261 return true;
262 }
263
264 /**
265 * Resolves a host to the distinct IPv4 and IPv6 addresses a request to it could reach.
266 *
267 * IP literals are returned as-is. An empty list means the host is malformed or resolved
268 * to nothing, and callers must treat it as unsafe.
269 *
270 * @since 0.7.0
271 *
272 * @param string $host Host name or IP literal (IPv6 literals may be bracketed).
273 * @return string[] List of IP addresses.
274 */
275 public static function resolve_host_ips( $host ) {
276 if ( ! is_string( $host ) || '' === $host ) {
277 return array();
278 }
279
280 // Unwrap one bracket pair only, so "]8.8.8.8[" can't become a clean address.
281 if ( preg_match( '/^\[(.*)\]$/', $host, $matches ) ) {
282 $host = $matches[1];
283 }
284
285 // Decode so "169%2e254%2e169%2e254" can't slip past the checks below.
286 $host = rawurldecode( $host );
287
288 // Strip an IPv6 zone id ("fe80::1%eth0"); a '%' on any other host is malformed.
289 if ( false !== strpos( $host, '%' ) ) {
290 if ( false === strpos( $host, ':' ) ) {
291 return array();
292 }
293 $host = preg_replace( '/%.*$/', '', $host );
294 }
295
296 /*
297 * Reject control bytes (gethostbynamel() throws on a NUL), stray brackets, and
298 * raw non-ASCII, which the request layer would punycode into a different host.
299 */
300 if ( '' === $host || preg_match( '/[\x00-\x20\x7f-\xff\[\]]/', $host ) ) {
301 return array();
302 }
303
304 if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
305 return array( $host );
306 }
307
308 $ips = array();
309
310 if ( function_exists( 'gethostbynamel' ) ) {
311 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- gethostbynamel() warns on an unresolvable host.
312 $ipv4 = @gethostbynamel( $host );
313 if ( is_array( $ipv4 ) ) {
314 $ips = $ipv4;
315 }
316 }
317
318 // gethostbynamel() only resolves IPv4; check AAAA records too. dns_get_record()
319 // can be disabled on some hosts and may warn on lookup failure.
320 if ( function_exists( 'dns_get_record' ) ) {
321 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- dns_get_record may fail on some systems.
322 $aaaa = @dns_get_record( $host, DNS_AAAA );
323 if ( is_array( $aaaa ) ) {
324 foreach ( $aaaa as $record ) {
325 if ( ! empty( $record['ipv6'] ) ) {
326 $ips[] = $record['ipv6'];
327 }
328 }
329 }
330 }
331
332 // Drop anything a resolver returned that is not an IP address.
333 $ips = array_filter(
334 $ips,
335 function ( $ip ) {
336 return is_string( $ip ) && false !== filter_var( $ip, FILTER_VALIDATE_IP );
337 }
338 );
339
340 return array_values( array_unique( $ips ) );
341 }
342
343 /**
344 * Validate an IP address.
345 *
346 * @param string $ip IP address.
347 * @return bool True if valid, false otherwise.
348 */
349 private static function validate_ip_address( string $ip ) {
350 return filter_var( $ip, FILTER_VALIDATE_IP );
351 }
352
353 /**
354 * Validate an array of IP addresses.
355 *
356 * @param array $ips List of IP addresses.
357 * @return bool True if all IPs are valid, false otherwise.
358 */
359 private static function validate_ip_addresses( array $ips ) {
360 foreach ( $ips as $ip ) {
361 if ( ! self::validate_ip_address( $ip ) ) {
362 return false;
363 }
364 }
365 return true;
366 }
367
368 /**
369 * Uses inet_pton if available to convert an IP address to a binary string.
370 * Returns false if an invalid IP address is given.
371 *
372 * @param mixed $ip IP address.
373 * @return int|string|bool
374 */
375 public static function convert_ip_address( $ip ) {
376 return inet_pton( $ip );
377 }
378
379 /**
380 * Determines the IP version of the given IP address.
381 *
382 * @param string $ip IP address.
383 * @return string|false 'ipv4', 'ipv6', or false if invalid.
384 */
385 public static function get_ip_version( $ip ) {
386 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
387 return 'ipv4';
388 } elseif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
389 return 'ipv6';
390 } else {
391 return false;
392 }
393 }
394
395 /**
396 * Extracts IP addresses from a given string.
397 *
398 * Supports IPv4 and IPv6 ranges in both hyphen and CIDR notation.
399 *
400 * @param string $ips List of IPs.
401 * @return array List of valid IP addresses or ranges.
402 */
403 public static function get_ip_addresses_from_string( $ips ) {
404 // Split the string by spaces, commas, and semicolons.
405 $ips = preg_split( '/[\s,;]/', (string) $ips );
406
407 $result = array();
408
409 foreach ( $ips as $ip ) {
410 $ip = trim( $ip );
411
412 // Check for CIDR notation
413 if ( strpos( $ip, '/' ) !== false ) {
414 if ( self::validate_cidr( $ip ) ) {
415 $result[] = $ip;
416 }
417 continue;
418 }
419
420 // Validate both IP values from the hyphen range.
421 $range = explode( '-', $ip );
422 if ( count( $range ) === 2 ) {
423 if ( self::validate_ip_range( $range[0], $range[1] ) ) {
424 $result[] = $ip;
425 }
426 continue;
427 }
428
429 // Validate the single IP value.
430 if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
431 $result[] = $ip;
432 }
433 }
434
435 return $result;
436 }
437
438 /**
439 * Validates CIDR notation for IPv4 and IPv6 addresses.
440 *
441 * @param string $cidr CIDR notation IP address.
442 * @return bool True if valid, false otherwise.
443 */
444 public static function validate_cidr( $cidr ) {
445 // Split the CIDR notation into IP address and prefix length using the '/' separator.
446 $parts = explode( '/', $cidr );
447 if ( count( $parts ) !== 2 ) {
448 return false; // Invalid CIDR notation if it doesn't contain exactly one '/'.
449 }
450
451 list( $ip, $netmask ) = $parts;
452
453 // Validate the IP address.
454 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
455 return false;
456 }
457
458 $ip_version = self::get_ip_version( $ip );
459 if ( ! $ip_version ) {
460 return false; // Invalid IP address.
461 }
462
463 // Validate the netmask based on the IP version.
464 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
465 return false;
466 }
467
468 return true;
469 }
470
471 /**
472 * Checks if an IP address is within a CIDR range.
473 * Supports both IPv4 and IPv6.
474 *
475 * @param string $ip IP address.
476 * @param string $cidr CIDR notation IP range.
477 * @return bool True if IP is within the range, false otherwise.
478 */
479 public static function ip_in_cidr( $ip, $cidr ) {
480 // Parse the CIDR notation to extract the base IP address and netmask prefix length.
481 $parsed_cidr = self::parse_cidr( $cidr );
482 if ( ! $parsed_cidr ) {
483 return false;
484 }
485 list( $range, $netmask ) = $parsed_cidr;
486
487 // Determine the IP version (IPv4 or IPv6) of both the input IP and the CIDR range IP.
488 $ip_version = self::get_ip_version( $ip );
489 $range_version = self::get_ip_version( $range );
490
491 // Ensure both IP addresses are valid and of the same IP version.
492 if ( ! $ip_version || ! $range_version || $ip_version !== $range_version ) {
493 return false;
494 }
495
496 // Validate the netmask based on the IP version.
497 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
498 return false;
499 }
500
501 if ( $ip_version === 'ipv4' ) {
502 return self::ip_in_ipv4_cidr( $ip, $range, $netmask );
503 } else {
504 return self::ip_in_ipv6_cidr( $ip, $range, $netmask );
505 }
506 }
507
508 /**
509 * Parses the CIDR notation into network address and netmask.
510 *
511 * @param string $cidr CIDR notation IP range.
512 * @return array|false Array containing network address and netmask, or false on failure.
513 */
514 public static function parse_cidr( $cidr ) {
515 $cidr_parts = explode( '/', $cidr, 2 );
516 if ( count( $cidr_parts ) !== 2 ) {
517 return false; // Invalid CIDR notation
518 }
519 list( $range, $netmask ) = $cidr_parts;
520
521 // Determine IP version
522 $ip_version = self::get_ip_version( $range );
523 if ( ! $ip_version ) {
524 return false; // Invalid IP address
525 }
526
527 // Validate netmask range
528 if ( ! self::validate_netmask( $netmask, $ip_version ) ) {
529 return false; // Netmask out of range
530 }
531
532 return array( $range, (int) $netmask );
533 }
534
535 /**
536 * Validates the netmask based on IP version.
537 *
538 * @param string|int $netmask Netmask value.
539 * @param string $ip_version 'ipv4' or 'ipv6'.
540 * @return bool True if valid, false otherwise.
541 */
542 public static function validate_netmask( $netmask, $ip_version ) {
543 // Ensure that $netmask is an integer
544 if ( ! ctype_digit( (string) $netmask ) ) {
545 return false;
546 }
547 $netmask = (int) $netmask;
548
549 // Validate the netmask based on the IP version.
550 if ( $ip_version === 'ipv4' ) {
551 return ( $netmask >= 0 && $netmask <= 32 );
552 } elseif ( $ip_version === 'ipv6' ) {
553 return ( $netmask >= 0 && $netmask <= 128 );
554 } else {
555 return false;
556 }
557 }
558
559 /**
560 * Checks if an IPv4 address is within a CIDR range.
561 *
562 * @param string $ip IPv4 address to check.
563 * @param string $range IPv4 network address.
564 * @param int $netmask Netmask value.
565 * @return bool True if IP is within the range, false otherwise.
566 */
567 public static function ip_in_ipv4_cidr( $ip, $range, $netmask ) {
568 // Validate arguments.
569 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv4' ) ) {
570 return false; // Invalid IP address or netmask.
571 }
572
573 // Convert IP addresses from their dotted representation to 32-bit unsigned integers.
574 $ip_long = ip2long( $ip );
575 $range_long = ip2long( $range );
576
577 // Check if the conversion was successful.
578 if ( $ip_long === false || $range_long === false ) {
579 return false; // One of the IP addresses is invalid.
580 }
581
582 /**
583 * Create the subnet mask as a 32-bit unsigned integer.
584 *
585 * Explanation:
586 * - (32 - $netmask) calculates the number of host bits (the bits not used for the network address).
587 * - (1 << (32 - $netmask)) shifts the number 1 left by the number of host bits.
588 * This results in a number where there is a single 1 followed by zeros equal to the number of host bits.
589 * - Subtracting 1 gives us a number where the host bits are all 1s.
590 * - Applying the bitwise NOT operator (~) inverts the bits, turning all host bits to 0 and network bits to 1.
591 * This results in the subnet mask having 1s in the network portion and 0s in the host portion.
592 *
593 * Example for netmask = 24:
594 * - (32 - 24) = 8
595 * - (1 << 8) = 256 (binary: 00000000 00000000 00000001 00000000)
596 * - 256 - 1 = 255 (binary: 00000000 00000000 00000000 11111111)
597 * - ~255 = 4294967040 (binary: 11111111 11111111 11111111 00000000)
598 */
599 $mask = ~ ( ( 1 << ( 32 - $netmask ) ) - 1 );
600
601 /**
602 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
603 * - ($ip_long & $mask) isolates the network portion of the IP address.
604 * - ($range_long & $mask) isolates the network portion of the CIDR range.
605 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
606 */
607 return ( $ip_long & $mask ) === ( $range_long & $mask );
608 }
609
610 /**
611 * Checks if an IPv6 address is within a CIDR range.
612 *
613 * @param string $ip IPv6 address to check.
614 * @param string $range IPv6 network address.
615 * @param int $netmask Netmask value.
616 * @return bool True if IP is within the range, false otherwise.
617 */
618 public static function ip_in_ipv6_cidr( $ip, $range, $netmask ) {
619 // Validate arguments.
620 if ( ! self::validate_ip_addresses( array( $ip, $range ) ) || ! self::validate_netmask( $netmask, 'ipv6' ) ) {
621 return false; // Invalid IP address or netmask.
622 }
623
624 // Convert IP addresses from their textual representation to binary strings.
625 $ip_bin = inet_pton( $ip );
626 $range_bin = inet_pton( $range );
627
628 // Check if the conversion was successful.
629 if ( $ip_bin === false || $range_bin === false ) {
630 return false; // One of the IP addresses is invalid.
631 }
632
633 /**
634 * Calculate the subnet mask in binary form.
635 *
636 * IPv6 addresses are 128 bits long.
637 * The netmask defines how many bits are set to 1 in the subnet mask.
638 *
639 * - $netmask_full_bytes: Number of full bytes (each 8 bits) that are all 1s.
640 * - $netmask_remainder_bits: Remaining bits (less than 8) that need to be set to 1.
641 *
642 * For example, if $netmask = 65:
643 * - $netmask_full_bytes = floor(65 / 8) = 8 (since 8 * 8 = 64 bits)
644 * - $netmask_remainder_bits = 65 % 8 = 1 (1 bit remaining)
645 *
646 * We'll construct the subnet mask by:
647 * - Starting with $netmask_full_bytes of 0xff (11111111 in binary).
648 * - Adding a byte where the first $netmask_remainder_bits bits are 1, rest are 0.
649 * - Padding the rest with zeros to make it 16 bytes (128 bits) long.
650 */
651
652 // Number of full bytes (each full byte is 8 bits) in the netmask.
653 $netmask_full_bytes = (int) ( $netmask / 8 );
654
655 // Number of remaining bits in the last byte of the netmask.
656 $netmask_remainder_bits = $netmask % 8;
657
658 // Start with a string of $netmask_full_bytes of 0xff bytes (each byte is 8 bits set to 1).
659 $netmask_bin = str_repeat( "\xff", $netmask_full_bytes );
660
661 if ( $netmask_remainder_bits > 0 ) {
662 // Create the last byte with $netmask_remainder_bits bits set to 1 from the left.
663 // - str_repeat('1', $netmask_remainder_bits): creates a string with the required number of '1's.
664 // - str_pad(...): pads the string on the right with '0's to make it 8 bits.
665 // - bindec(...): converts the binary string to a decimal number.
666 // - chr(...): gets the character corresponding to the byte value.
667 $last_byte = chr( bindec( str_pad( str_repeat( '1', $netmask_remainder_bits ), 8, '0', STR_PAD_RIGHT ) ) );
668 // Append the last byte to the netmask binary string.
669 $netmask_bin .= $last_byte;
670 }
671
672 // Pad the netmask binary string to 16 bytes (128 bits) with zeros (\x00).
673 $netmask_bin = str_pad( $netmask_bin, 16, "\x00" );
674
675 /**
676 * Use bitwise AND to apply the subnet mask to both the IP address and the network address.
677 * - ($ip_bin & $netmask_bin) isolates the network portion of the IP address.
678 * - ($range_bin & $netmask_bin) isolates the network portion of the CIDR range.
679 * - If both network portions are equal, the IP address belongs to the same subnet and is within the CIDR range.
680 */
681 return ( $ip_bin & $netmask_bin ) === ( $range_bin & $netmask_bin );
682 }
683
684 /**
685 * Validates the low and high IP addresses of a range.
686 *
687 * Now supports IPv6 addresses.
688 *
689 * @param string $range_low Low IP address.
690 * @param string $range_high High IP address.
691 * @return bool True if the range is valid, false otherwise.
692 */
693 public static function validate_ip_range( $range_low, $range_high ) {
694 // Validate that both IP addresses are valid.
695 if ( self::validate_ip_addresses( array( $range_low, $range_high ) ) === false ) {
696 return false;
697 }
698
699 // Ensure both IPs are of the same version
700 $range_low_ip_version = self::get_ip_version( $range_low );
701 $range_high_ip_version = self::get_ip_version( $range_high );
702
703 if ( $range_low_ip_version !== $range_high_ip_version || ! $range_low_ip_version || ! $range_high_ip_version ) {
704 return false; // Invalid or mixed IP versions.
705 }
706
707 // Convert IP addresses to their packed binary representation.
708 $ip_low = inet_pton( $range_low );
709 $ip_high = inet_pton( $range_high );
710
711 // Check if the conversion was successful.
712 if ( false === $ip_low || false === $ip_high ) {
713 return false;
714 }
715
716 // Compare the binary representations to ensure the low IP is not greater than the high IP.
717 if ( strcmp( $ip_low, $ip_high ) > 0 ) {
718 return false;
719 }
720
721 return true;
722 }
723
724 /**
725 * Checks that a given IP address is within a given range.
726 *
727 * Supports CIDR notation and hyphenated ranges for both IPv4 and IPv6.
728 *
729 * @param string $ip IP address.
730 * @param string $range_low Range low or CIDR notation.
731 * @param null|string $range_high Optional. Range high. Not used if $range_low is CIDR notation.
732 * @return bool
733 */
734 public static function ip_address_is_in_range( $ip, $range_low, $range_high = null ) {
735 // Validate that all provided IP addresses are valid.
736 if ( $range_high !== null && ! self::validate_ip_addresses( array( $ip, $range_low, $range_high ) ) ) {
737 return false;
738 } else {
739 $range_low_parsed = self::parse_cidr( $range_low );
740 if ( $range_low_parsed && ! self::validate_ip_addresses( array( $ip, $range_low_parsed[0] ) ) ) {
741 return false;
742 }
743 }
744
745 if ( strpos( $range_low, '/' ) !== false ) {
746 // CIDR notation
747 if ( $range_high !== null ) {
748 // Invalid usage: CIDR notation with range high parameter
749 return false;
750 }
751 return self::ip_in_cidr( $ip, $range_low );
752 }
753
754 // Hyphenated range
755 if ( $range_high === null ) {
756 return false; // Invalid parameters
757 }
758
759 $ip_num = inet_pton( $ip );
760 $ip_low = inet_pton( $range_low );
761 $ip_high = inet_pton( $range_high );
762 if ( $ip_num && $ip_low && $ip_high && strcmp( $ip_num, $ip_low ) >= 0 && strcmp( $ip_num, $ip_high ) <= 0 ) {
763 return true;
764 }
765 return false;
766 }
767 }
768