PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 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 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-ip/src/class-utils.php +587 -66 12.1.3 → 16.3-beta View file →
@@ -11,9 +11,9 @@
11 11 * Class that provides static methods for working with IP addresses.
12 12 */
13 13 class Utils {
14 14
15 - const PACKAGE_VERSION = '0.1.2';
15 + const PACKAGE_VERSION = '0.7.0';
16 16
17 17 /**
18 18 * Get the current user's IP address.
19 19 *
@@ -86,13 +86,14 @@
86 86
87 87 /**
88 88 * Checks an IP to see if it is within a private range.
89 89 *
90 - * @param int $ip IP address.
91 - * @return bool True if IP address is private, false otherwise.
90 + * @param string $ip IP address.
91 + * @return bool True if IP address is private, false otherwise.
92 92 */
93 93 public static function ip_is_private( $ip ) {
94 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.
95 96 if ( false === strpos( $ip, '.' ) ) {
96 97 return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
97 98 }
98 99 // We are dealing with ipv4.
@@ -115,76 +116,309 @@
115 116 return false;
116 117 }
117 118
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 + /**
119 369 * Uses inet_pton if available to convert an IP address to a binary string.
120 - * If inet_pton is not available, ip2long will convert the address to an integer.
121 370 * Returns false if an invalid IP address is given.
122 371 *
123 - * NOTE: ip2long will return false for any ipv6 address. servers that do not support
124 - * inet_pton will not support ipv6
125 - *
126 372 * @param mixed $ip IP address.
127 373 * @return int|string|bool
128 374 */
129 375 public static function convert_ip_address( $ip ) {
130 - if ( function_exists( 'inet_pton' ) ) {
131 - return inet_pton( $ip );
132 - }
133 - return ip2long( $ip );
376 + return inet_pton( $ip );
134 377 }
135 378
136 379 /**
137 - * Checks that a given IP address is within a given low - high range.
138 - * Servers that support inet_pton will use that function to convert the ip to number,
139 - * while other servers will use ip2long.
380 + * Determines the IP version of the given IP address.
140 381 *
141 - * NOTE: servers that do not support inet_pton cannot support ipv6.
142 - *
143 - * @param mixed $ip IP.
144 - * @param mixed $range_low Range Low.
145 - * @param mixed $range_high Range High.
146 - * @return Bool
382 + * @param string $ip IP address.
383 + * @return string|false 'ipv4', 'ipv6', or false if invalid.
147 384 */
148 - public static function ip_address_is_in_range( $ip, $range_low, $range_high ) {
149 - // The inet_pton will give us binary string of an ipv4 or ipv6.
150 - // We can then use strcmp to see if the address is in range.
151 - if ( function_exists( 'inet_pton' ) ) {
152 - $ip_num = inet_pton( $ip );
153 - $ip_low = inet_pton( $range_low );
154 - $ip_high = inet_pton( $range_high );
155 - if ( $ip_num && $ip_low && $ip_high && strcmp( $ip_num, $ip_low ) >= 0 && strcmp( $ip_num, $ip_high ) <= 0 ) {
156 - return true;
157 - }
158 - // The ip2long will give us an integer of an ipv4 address only. it will produce FALSE for ipv6.
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';
159 390 } else {
160 - $ip_num = ip2long( $ip );
161 - $ip_low = ip2long( $range_low );
162 - $ip_high = ip2long( $range_high );
163 - if ( $ip_num && $ip_low && $ip_high && $ip_num >= $ip_low && $ip_num <= $ip_high ) {
164 - return true;
165 - }
391 + return false;
166 392 }
167 - return false;
168 393 }
169 394
170 395 /**
171 396 * Extracts IP addresses from a given string.
172 397 *
173 - * We allow for both, one IP per line or comma-; semicolon; or whitespace-separated lists. This also validates the IP addresses
174 - * and only returns the ones that look valid. IP ranges using the "-" character are also supported.
398 + * Supports IPv4 and IPv6 ranges in both hyphen and CIDR notation.
175 399 *
176 - * @param string $ips List of ips - example: "8.8.8.8\n4.4.4.4,2.2.2.2;1.1.1.1 9.9.9.9,5555.5555.5555.5555,1.1.1.10-1.1.1.20".
177 - * @return array List of valid IP addresses. - example based on input example: array('8.8.8.8', '4.4.4.4', '2.2.2.2', '1.1.1.1', '9.9.9.9', '1.1.1.10-1.1.1.20')
400 + * @param string $ips List of IPs.
401 + * @return array List of valid IP addresses or ranges.
178 402 */
179 403 public static function get_ip_addresses_from_string( $ips ) {
180 - $ips = (string) $ips;
181 - $ips = preg_split( '/[\s,;]/', $ips );
404 + // Split the string by spaces, commas, and semicolons.
405 + $ips = preg_split( '/[\s,;]/', (string) $ips );
182 406
183 407 $result = array();
184 408
185 409 foreach ( $ips as $ip ) {
186 - // Validate both IP values from the range.
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.
187 421 $range = explode( '-', $ip );
188 422 if ( count( $range ) === 2 ) {
189 423 if ( self::validate_ip_range( $range[0], $range[1] ) ) {
190 424 $result[] = $ip;
@@ -201,11 +435,257 @@
201 435 return $result;
202 436 }
203 437
204 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 + /**
205 685 * Validates the low and high IP addresses of a range.
206 686 *
207 - * NOTE: servers that do not support inet_pton cannot support ipv6.
687 + * Now supports IPv6 addresses.
208 688 *
209 689 * @param string $range_low Low IP address.
210 690 * @param string $range_high High IP address.
211 691 * @return bool True if the range is valid, false otherwise.
@@ -211,36 +691,77 @@
211 691 * @return bool True if the range is valid, false otherwise.
212 692 */
213 693 public static function validate_ip_range( $range_low, $range_high ) {
214 694 // Validate that both IP addresses are valid.
215 - if ( ! filter_var( $range_low, FILTER_VALIDATE_IP ) || ! filter_var( $range_high, FILTER_VALIDATE_IP ) ) {
695 + if ( self::validate_ip_addresses( array( $range_low, $range_high ) ) === false ) {
216 696 return false;
217 697 }
218 698
219 - // Validate that the $range_low is lower or equal to $range_high.
220 - if ( function_exists( 'inet_pton' ) ) {
221 - // The inet_pton will give us binary string of an ipv4 or ipv6.
222 - // We can then use strcmp to see if the address is in range.
223 - $ip_low = inet_pton( $range_low );
224 - $ip_high = inet_pton( $range_high );
225 - if ( false === $ip_low || false === $ip_high ) {
226 - return false;
227 - }
228 - if ( strcmp( $ip_low, $ip_high ) > 0 ) {
229 - return false;
230 - }
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;
231 738 } else {
232 - // The ip2long will give us an integer of an ipv4 address only. it will produce FALSE for ipv6.
233 - $ip_low = ip2long( $range_low );
234 - $ip_high = ip2long( $range_high );
235 - if ( false === $ip_low || false === $ip_high ) {
739 + $range_low_parsed = self::parse_cidr( $range_low );
740 + if ( $range_low_parsed && ! self::validate_ip_addresses( array( $ip, $range_low_parsed[0] ) ) ) {
236 741 return false;
237 742 }
238 - if ( $ip_low > $ip_high ) {
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
239 749 return false;
240 750 }
751 + return self::ip_in_cidr( $ip, $range_low );
241 752 }
242 753
243 - return true;
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;
244 766 }
245 -
246 767 }