/public/wp-content/plugins/better-wp-security/core/lib/class-itsec-lib-ip-tools.php

https://gitlab.com/kath.de/cibedo_cibedo.de · PHP · 649 lines · 270 code · 80 blank · 299 comment · 62 complexity · 063d67ff83ad8fef9fd338a2ee6b470a MD5 · raw file

  1. <?php
  2. /**
  3. * iThemes Security IP tools library.
  4. *
  5. * Contains the ITSEC_Lib_IP_Tools class.
  6. *
  7. * @package iThemes_Security
  8. */
  9. /**
  10. * iThemes Security IP Tools Library class.
  11. *
  12. * Utility class for validating and comparing IPs, as well as converting ranges. Supports IPv4 and IPv6.
  13. *
  14. * @package iThemes_Security
  15. * @since 2.2.0
  16. */
  17. class ITSEC_Lib_IP_Tools {
  18. /**
  19. * Stores max cidr (number of bits) for each IP version.
  20. *
  21. * @static
  22. * @access private
  23. *
  24. * @var array
  25. */
  26. private static $_max_cidr = array(
  27. 4 => 32,
  28. 6 => 128,
  29. );
  30. /**
  31. * Validates an IP or an IP Range using CIDR notation
  32. *
  33. * @since 2.2.0
  34. *
  35. * @static
  36. * @access public
  37. *
  38. * @param string $ip The IP address to validate, can be given in CIDR notation
  39. *
  40. * @return bool|int False for an invalid IP or range, and the IP version (4 or 6) on for a valid one
  41. */
  42. public static function validate( $ip ) {
  43. $ip_parts = self::_ip_cidr( $ip );
  44. if ( filter_var( $ip_parts->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
  45. if ( ! isset( $ip_parts->cidr ) || self::_is_valid_cidr( $ip_parts->cidr, 4 ) ) {
  46. return 4;
  47. }
  48. // Invalid CIDR
  49. return false;
  50. } elseif ( filter_var( $ip_parts->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
  51. if ( ! isset( $ip_parts->cidr ) || self::_is_valid_cidr( $ip_parts->cidr, 6 ) ) {
  52. return 6;
  53. }
  54. // Invalid CIDR
  55. return false;
  56. }
  57. // IP is not valid v4 or v6 IP
  58. return false;
  59. }
  60. /**
  61. * Converts an IP or an IP Range using CIDR notation, to it's parts (IP and CIDR)
  62. *
  63. * @since 2.2.0
  64. *
  65. * @static
  66. * @access private
  67. *
  68. * @param string $ip The IP address, can be given in CIDR notation
  69. *
  70. * @return object IP parts, ->ip and ->cidr
  71. */
  72. private static function _ip_cidr( $ip ) {
  73. $ip_parts = new stdClass();
  74. if ( strpos( $ip, '/' ) ) {
  75. list( $ip_parts->ip, $ip_parts->cidr ) = explode( '/', $ip );
  76. } else {
  77. $ip_parts->ip = $ip;
  78. }
  79. return $ip_parts;
  80. }
  81. /**
  82. * Validates a CIDR value for an IP version
  83. *
  84. * @since 2.2.0
  85. *
  86. * @static
  87. * @access private
  88. *
  89. * @param string $cidr The CIDR value to validate
  90. * @param int $version The IP version to validate the CIDR for (4 or 6)
  91. *
  92. * @return bool
  93. */
  94. private static function _is_valid_cidr( $cidr, $version ) {
  95. // $version needs to be valid
  96. if ( ! in_array( $version, array_keys( self::$_max_cidr ) ) ) {
  97. return false;
  98. }
  99. // The cidr needs to be numeric and between 0 and the max
  100. if ( isset( $cidr ) && ( ! ctype_digit( $cidr ) || $cidr > self::$_max_cidr[ $version ] ) ) {
  101. return false;
  102. }
  103. return true;
  104. }
  105. /**
  106. * Checks to see if a given IP/CIDR is a range
  107. *
  108. * @since 2.2.0
  109. *
  110. * @static
  111. * @access public
  112. *
  113. * @param string $ip The IP address, can be given in CIDR notation
  114. * @param int $version The IP version (4 or 6). This needs to be supplied if skipping validation (for efficiency)
  115. * @param bool $validate True to validate the IP, and false to skip (version must be supplied to skip) (Default true)
  116. *
  117. * @return bool
  118. */
  119. public static function is_range( $ip, $version = null, $validate = true ) {
  120. if ( $validate || ! isset( $version ) ) {
  121. $version = self::validate( $ip );
  122. // If the IP isn't valid, it's not a range.
  123. if ( ! $version ) {
  124. return false;
  125. }
  126. }
  127. $ip_parts = self::_ip_cidr( $ip );
  128. // If there is no cidr specified or if it's the max for this IP version, then this is not a range.
  129. return !( ! isset( $ip_parts->cidr ) || $ip_parts->cidr == self::$_max_cidr[ $version ] );
  130. }
  131. /**
  132. * Gets the start and end IPs for a given range
  133. *
  134. * @static
  135. * @access public
  136. *
  137. * @param string $ip The IP address, can be given in CIDR notation
  138. *
  139. * @return bool|array False if the IP is invalid, and an array containing start and end IPs for the range specified otherwise
  140. */
  141. public static function get_ip_range( $ip ) {
  142. $version = self::validate( $ip );
  143. if ( ! $version ) {
  144. return false;
  145. }
  146. $ip_parts = self::_ip_cidr( $ip );
  147. // If this isn't a range, return a single address
  148. if ( ! self::is_range( $ip, $version, false ) ) {
  149. return array(
  150. 'start' => $ip_parts->ip,
  151. 'end' => $ip_parts->ip,
  152. );
  153. }
  154. $mask = self::get_mask( $ip_parts->cidr, $version );
  155. $range = array();
  156. $range['start'] = inet_ntop( inet_pton( $ip_parts->ip ) & inet_pton( $mask ) );
  157. $range['end'] = inet_ntop( inet_pton( $ip_parts->ip ) | ~ inet_pton( $mask ) );
  158. return $range;
  159. }
  160. /**
  161. * Gets the mask from CIDR and IP version
  162. *
  163. * @static
  164. * @access public
  165. *
  166. * @param string $cidr The CIDR value to validate
  167. * @param int $version The IP version to validate the CIDR for (4 or 6)
  168. *
  169. * @return string IP Mask
  170. */
  171. public static function get_mask( $cidr, $version ) {
  172. if ( ! in_array( $version, array( 4, 6 ) ) ) {
  173. return false;
  174. }
  175. $bin_mask = str_repeat( '1', $cidr ) . str_repeat( '0', self::$_max_cidr[ $version ] - $cidr );
  176. $bin2mask_method = '_bin2mask_v' . $version;
  177. return call_user_func( array( 'self', $bin2mask_method ), $bin_mask );
  178. }
  179. /**
  180. * Gets the IPv4 mask from the binary representation
  181. *
  182. * @static
  183. * @access private
  184. *
  185. * @param string $bin_mask The binary representation of the mask
  186. *
  187. * @return string IP Mask
  188. */
  189. private static function _bin2mask_v4( $bin_mask ) {
  190. $mask = array();
  191. // Eight binary bits per number
  192. foreach ( str_split( $bin_mask, 8 ) as $num ) {
  193. // Convert from bin to dec and append
  194. $mask[] = base_convert( $num, 2, 10 );
  195. }
  196. // Explode our new hex mask into 4 character segments and implode with colons
  197. return implode( '.', $mask );
  198. }
  199. /**
  200. * Gets the IPv6 mask from the binary representation
  201. *
  202. * @static
  203. * @access private
  204. *
  205. * @param string $bin_mask The binary representation of the mask
  206. *
  207. * @return string IP Mask
  208. */
  209. private static function _bin2mask_v6( $bin_mask ) {
  210. $mask = '';
  211. // Four binary bits per hex character
  212. foreach ( str_split( $bin_mask, 4 ) as $char ) {
  213. // Convert from bin to hex and append
  214. $mask .= base_convert( $char, 2, 16 );
  215. }
  216. // Explode our new hex mask into 4 character segments and implode with colons
  217. return implode( ':', str_split( $mask, 4 ) );
  218. }
  219. /**
  220. * Checks to see if an IP or range is within another IP or range
  221. *
  222. * @static
  223. * @access public
  224. *
  225. * @param string $ip The IP address to check to see if is contained, can be given in CIDR notation
  226. * @param string $range The IP address to check to see if contains, can be given in CIDR notation
  227. *
  228. * @return bool False if the given IP or range is not completely contained in the supplied range. True if it is
  229. */
  230. public static function in_range( $ip, $range ) {
  231. $ip_version = self::validate( $ip );
  232. // If the IP isn't valid, it's not in the range
  233. if ( ! $ip_version ) {
  234. return false;
  235. }
  236. $range_version = self::validate( $range );
  237. // If the range isn't valid or isn't the same IP version as the first IP, it's not in the range
  238. if ( $ip_version != $range_version ) {
  239. return false;
  240. }
  241. if ( ! self::is_range( $range, $range_version, false ) ) {
  242. if ( ! self::is_range( $ip, $ip_version, false ) ) {
  243. $ip_parts = self::_ip_cidr( $ip );
  244. $range_parts = self::_ip_cidr( $ip );
  245. // If neither is a range, just compare and return
  246. return $ip_parts->ip == $range_parts->ip;
  247. } else {
  248. // If the IP is a range and the specified range isn't, then return false
  249. return false;
  250. }
  251. }
  252. $ip_range = array_map( 'inet_pton', self::get_ip_range( $ip ) );
  253. $in_range = array_map( 'inet_pton', self::get_ip_range( $range ) );
  254. return ( $in_range['start'] <= $ip_range['start'] && $ip_range['end'] <= $in_range['end'] );
  255. }
  256. /**
  257. * Checks to see if an IP or range intersects with another IP or range
  258. *
  259. * @static
  260. * @access public
  261. *
  262. * @param string $ip1 IP address, can be given in CIDR notation
  263. * @param string $ip2 IP address, can be given in CIDR notation
  264. *
  265. * @return bool
  266. */
  267. public static function intersect( $ip1, $ip2 ) {
  268. $ip1_version = self::validate( $ip1 );
  269. // If the first IP isn't valid, there is no intersection
  270. if ( ! $ip1_version ) {
  271. return false;
  272. }
  273. $ip2_version = self::validate( $ip2 );
  274. // If the second IP isn't valid or isn't the same IP version as the first IP, there is no intersection
  275. if ( $ip1_version != $ip2_version ) {
  276. return false;
  277. }
  278. // If neither is a range, just compare and return
  279. if ( ! self::is_range( $ip1, $ip1_version, false ) && ! self::is_range( $ip2, $ip2_version, false ) ) {
  280. $ip1_parts = self::_ip_cidr( $ip1 );
  281. $ip2_parts = self::_ip_cidr( $ip2 );
  282. return $ip1_parts->ip == $ip2_parts->ip;
  283. }
  284. $ip1_range = array_map( 'inet_pton', self::get_ip_range( $ip1 ) );
  285. $ip2_range = array_map( 'inet_pton', self::get_ip_range( $ip2 ) );
  286. return (
  287. // $ip1_range start is in $ip2_range
  288. ( $ip2_range['start'] <= $ip1_range['start'] && $ip1_range['start'] <= $ip2_range['end'] ) ||
  289. // $ip1_range end is in $ip2_range
  290. ( $ip2_range['start'] <= $ip1_range['end'] && $ip1_range['end'] <= $ip2_range['end'] ) ||
  291. // $ip2_range start is in $ip1_range
  292. ( $ip1_range['start'] <= $ip2_range['start'] && $ip2_range['start'] <= $ip1_range['end'] ) ||
  293. // $ip2_range end is in $ip1_range
  294. ( $ip1_range['start'] <= $ip2_range['end'] && $ip2_range['end'] <= $ip1_range['end'] )
  295. );
  296. }
  297. /**
  298. * Converts IP with * wildcards to CIDR format
  299. *
  300. * Limited to only contiguous wildcards at the end of an IP, and wildcards represent a whole segment not a single character or digit
  301. *
  302. * @since 2.2.0
  303. *
  304. * @static
  305. * @access public
  306. *
  307. * @param string $ip The IP address, can be given in CIDR notation
  308. * @param int $version The IP version (4 or 6). This needs to be supplied if skipping validation (for efficiency)
  309. * @param bool $validate True to validate the IP, and false to skip (version must be supplied to skip) (Default true)
  310. *
  311. * @return string IP in CIDR format
  312. */
  313. public static function ip_wild_to_ip_cidr( $ip, $version = null, $validate = true ) {
  314. if ( $validate || ! isset( $version ) ) {
  315. // Replace the wildcards with zeroes and test to get version
  316. $version = self::validate( self::_clean_wildcards( $ip ) );
  317. // If the IP isn't valid, it's not a range.
  318. if ( ! $version ) {
  319. return false;
  320. }
  321. }
  322. // Not meant for IPs already using CIDR notation and only works on wildcards
  323. if ( strpos( $ip, '/' ) || false === strpos( $ip, '*' ) ) {
  324. return $ip;
  325. }
  326. $wild_to_cidr_method = "_ipv{$version}_wild_to_ip_cidr";
  327. return call_user_func( array( 'self', $wild_to_cidr_method ), $ip );
  328. }
  329. /**
  330. * Converts IPv4 IP with * wildcards to CIDR format
  331. *
  332. * Limited to only contiguous wildcards at the end of an IP, and wildcards represent a whole segment not a single character or digit
  333. *
  334. * @since 2.2.0
  335. *
  336. * @static
  337. * @access private
  338. *
  339. * @param string $ip The IP address, can be given in CIDR notation
  340. *
  341. * @return string IP in CIDR format
  342. */
  343. private static function _ipv4_wild_to_ip_cidr( $ip ) {
  344. $host_parts = array_reverse( explode( '.', trim( $ip ) ) );
  345. $mask = self::$_max_cidr[4];
  346. $ip = self::_clean_wildcards( $ip );
  347. //convert hosts with wildcards to host with netmask and create rule lines
  348. foreach ( $host_parts as $part ) {
  349. if ( '*' === $part ) {
  350. $mask -= 8;
  351. } else {
  352. break; // We only want to deal with contiguous wildcards at the end of an IP
  353. }
  354. }
  355. return "{$ip}/{$mask}";
  356. }
  357. /**
  358. * Converts IPv6 IP with * wildcards to CIDR format
  359. *
  360. * Limited to only contiguous wildcards at the end of an IP, and wildcards represent a whole segment not a single character or digit
  361. *
  362. * @since 2.2.0
  363. *
  364. * @static
  365. * @access private
  366. *
  367. * @param string $ip The IP address, can be given in CIDR notation
  368. *
  369. * @return string IP in CIDR format
  370. */
  371. private static function _ipv6_wild_to_ip_cidr( $ip ) {
  372. $host_parts = array_reverse( explode( ':', trim( $ip ) ) );
  373. $mask = self::$_max_cidr[6];
  374. $ip = self::_clean_wildcards( $ip );
  375. //convert hosts with wildcards to host with netmask and create rule lines
  376. foreach ( $host_parts as $part ) {
  377. if ( '*' === $part ) {
  378. $mask -= 16;
  379. } else {
  380. break; // We only want to deal with contiguous wildcards at the end of an IP
  381. }
  382. }
  383. return "{$ip}/{$mask}";
  384. }
  385. /**
  386. * Remove wildcards, but only those that represent an entire chunk or octets
  387. *
  388. * @param string $ip The IP to clean
  389. *
  390. * @return string IP address with wildcards replaced with 0s
  391. */
  392. private static function _clean_wildcards( $ip ) {
  393. $search = array(
  394. '/([:\.])(\*(\1|$))+/', // Match all whole chunks in the middle with wildcards, or a wildcard as the whole chunk at the end
  395. '/^\*([:\.])/', // Match a wildcard as the whole first chunk
  396. );
  397. return preg_replace_callback( $search, array( 'self', 'clean_wildcards_preg_replace_callback' ), $ip );
  398. }
  399. /**
  400. * Used with preg_replace_callback() to replace wildcards with 0 ONLY in cases where the wildcard is the whole chunk
  401. *
  402. * @param array $matches The matches found by preg_replace_callback()
  403. *
  404. * @return string Replacement string
  405. */
  406. public static function clean_wildcards_preg_replace_callback( $matches ) {
  407. return str_replace( '*', '0', $matches[0] );
  408. }
  409. /**
  410. * Converts IP in CIDR notation to a regex
  411. *
  412. * @since 2.2.0
  413. *
  414. * @static
  415. * @access public
  416. *
  417. * @param string $ip The IP address, can be given in CIDR notation
  418. * @param int $version The IP version (4 or 6). This needs to be supplied if skipping validation (for efficiency)
  419. * @param bool $validate True to validate the IP, and false to skip (version must be supplied to skip) (Default true)
  420. *
  421. * @return string The IP in regex format
  422. */
  423. public static function ip_cidr_to_ip_regex( $ip, $version = null, $validate = true ) {
  424. // Not meant for IPs already using wildcards
  425. if ( strpos( $ip, '*' ) ) {
  426. return $ip;
  427. }
  428. if ( $validate || ! isset( $version ) ) {
  429. $version = self::validate( $ip );
  430. // If the IP isn't valid, it's not a range.
  431. if ( ! $version ) {
  432. return false;
  433. }
  434. }
  435. $ip_parts = self::_ip_cidr( $ip );
  436. $cidr_to_wild_method = "_ipv{$version}_cidr_to_ip_regex";
  437. return call_user_func( array( 'self', $cidr_to_wild_method ), $ip_parts );
  438. }
  439. /**
  440. * Converts IPv4 in CIDR notation to a regex
  441. *
  442. * @since 2.2.0
  443. *
  444. * @static
  445. * @access private
  446. *
  447. * @param object $ip_parts The IP address parts (->ip and ->cidr), generated using self::_ip_cidr()
  448. *
  449. * @return string The IP in regex format
  450. */
  451. private static function _ipv4_cidr_to_ip_regex( $ip_parts ) {
  452. // Explode IP into octets and reverse them to work backwards
  453. $octets = array_reverse( explode( '.', $ip_parts->ip ) );
  454. if ( ! isset( $ip_parts->cidr ) ) {
  455. $ip_parts->cidr = self::$_max_cidr[4];
  456. }
  457. // How many bits are actually masked
  458. $masked_bits = self::$_max_cidr[4] - $ip_parts->cidr;
  459. $i = 0;
  460. // For each set of 8 masked bits, we match a whole octet (3 digits is good enough here)
  461. while ( $masked_bits >= 8 ) {
  462. $octets[ $i ] = '[0-9]{1,3}';
  463. $masked_bits -= 8;
  464. ++$i;
  465. }
  466. // If there are still masked bits to deal with after handling all whole octets
  467. if ( $masked_bits ) {
  468. // The step is the gap between the low and high values for this octet
  469. $step = base_convert( str_repeat( '1', $masked_bits ), 2, 10 ) + 1;
  470. // $low is the low value for this octect, based on the step
  471. $low = $octets[ $i ] - ( $octets[ $i ] % $step );
  472. // The regex we use is simply a valid range of numbers in a group with alternation, ex: (0|1|2|3|4|5|6|7)
  473. $octets[ $i ] = '(' . implode( '|', range( $low, $low + $step - 1 ) ) . ')';
  474. }
  475. // Re-reverse the octets array to set things straight, and put the pieces back together
  476. // Escape the . for a literal .
  477. return implode( '\.', array_reverse( $octets ) );
  478. }
  479. /**
  480. * Converts IPv6 in CIDR notation to a regex
  481. *
  482. * @since 2.2.0
  483. *
  484. * @static
  485. * @access private
  486. *
  487. * @param object $ip_parts The IP address parts (->ip and ->cidr), generated using self::_ip_cidr()
  488. *
  489. * @return string The IP in regex format
  490. */
  491. private static function _ipv6_cidr_to_ip_regex( $ip_parts ) {
  492. // If the IP address has a :: in it, we need to expand that out so we have all eight chuks to work with
  493. $colons = substr_count( $ip_parts->ip, ':' );
  494. if ( $colons < 7 ) {
  495. // Fill out all the chunks so we can properly mask them all
  496. $ip_parts->ip = str_replace( '::', str_repeat( ':0', 7 - $colons + 1 ) . ':', $ip_parts->ip );
  497. }
  498. // Explode IP into chunks and reverse them to work backwards
  499. $chunks = array_reverse( explode( ':', $ip_parts->ip ) );
  500. if ( ! isset( $ip_parts->cidr ) ) {
  501. $ip_parts->cidr = self::$_max_cidr[6];
  502. }
  503. $masked_bits = self::$_max_cidr[6] - $ip_parts->cidr;
  504. $i = 0;
  505. // For each set of 16 masked bits, we match a whole chunk (1-4 hex characters)
  506. while ( $masked_bits >= 16 ) {
  507. $chunks[ $i ] = '[0-f]{1,4}';
  508. $masked_bits -= 16;
  509. ++$i;
  510. }
  511. // If there are still masked bits to deal with after handling all whole chunks, we start working in single hex characters
  512. if ( $masked_bits ) {
  513. // Explode the chunk into characters and reverse them to work backwards
  514. $characters = array_reverse( str_split( str_pad( $chunks[ $i ], 4, '0', STR_PAD_LEFT ) ) );
  515. $j = 0;
  516. // For each set of 4 masked bits, we match a single hex character
  517. while ( $masked_bits >= 4 ) {
  518. $characters[ $j ] = '[0-f]';
  519. $masked_bits -= 4;
  520. ++$j;
  521. }
  522. // If there are still masked bits to deal with after handling all whole characters
  523. if ( $masked_bits ) {
  524. // $step is the gap between the low and high values for this hex character (we want this in base 10 for use in operations)
  525. $step = base_convert( str_repeat( '1', $masked_bits ), 2, 10 ) + 1;
  526. // $value is the current value of the character in base 10
  527. $value = base_convert( $characters[ $j ], 16, 10 );
  528. // $low is the base 10 representation of the low value for this character based on the step
  529. $low = $value - ( $value % $step );
  530. // $high is the hex value (redy for our regex) of the high value for this character
  531. $high = base_convert( $low + $step - 1, 10, 16 );
  532. // Convert $low to hex for our
  533. $low = base_convert( $low, 10, 16 );
  534. // For our regex we use a character set from low to high, ex: [4-7] or [8-b]
  535. $characters[ $j ] = "[{$low}-{$high}]";
  536. }
  537. // Re-reverse the characters array to set things straight, and put the pieces back together
  538. $chunks[ $i ] = implode( array_reverse( $characters ) );
  539. $zeroes = strlen( $chunks[ $i ] ) - strlen( ltrim( $chunks[ $i ], '0' ) );
  540. if ( $zeroes ) {
  541. $chunks[ $i ] = str_repeat( '0?', $zeroes ) . ltrim( $chunks[ $i ], '0' );
  542. }
  543. }
  544. for ( $i; $i < count( $chunks ); $i++ ) {
  545. $chunks[ $i ] = ltrim( $chunks[ $i ], '0' );
  546. $num_chars = strlen( $chunks[ $i ] );
  547. if ( $num_chars < 4 ) {
  548. $chunks[ $i ] = str_repeat( '0?', 4 - $num_chars ) . $chunks[ $i ];
  549. }
  550. }
  551. // Re-reverse the chunks array to set things straight, and put the pieces back together
  552. $regex = implode( ':', array_reverse( $chunks ) );
  553. // Replace multiple chunks of all zeros with a regular expression that makes them optional but still enforces accurate matching
  554. $regex = preg_replace_callback( '/0\?0\?0\?0\?(\:0\?0\?0\?0\?)+/', array( 'self', 'ipv6_regex_preg_replace_callback' ), $regex );
  555. return $regex;
  556. }
  557. /**
  558. * Used with preg_replace_callback() to make chunks of all zeroes optional while still enforcing accurate matching
  559. *
  560. * @param array $matches The matches found by preg_replace_callback()
  561. *
  562. * @return string Replacement string
  563. */
  564. public static function ipv6_regex_preg_replace_callback( $matches ) {
  565. // Get the number of colons (chunks - 1) that we are replacing so we make sure to match no more than the original number of chunks
  566. $colons = substr_count( $matches[0], ':' );
  567. return sprintf( '(0{0,4}:){0,%d}(0{0,4})?', $colons );
  568. }
  569. }