PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / traits / ip.php

ip.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/traits/ip.php

549 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper functions for IP related tasks.
4 *
5 * @package WP_Defender\Traits
6 */
7
8 namespace WP_Defender\Traits;
9
10 use WP_Defender\Component\Http\Remote_Address;
11 use WP_Defender\Component\Smart_Ip_Detection;
12 use WP_Defender\Model\Setting\Firewall;
13
14 trait IP {
15
16 /**
17 * Check if the IP is IPv4 address.
18 *
19 * @param string $ip The IP address to check.
20 *
21 * @return bool Returns true if the IP address is an IPv4 address, false otherwise.
22 */
23 private function is_v4( $ip ) {
24 return false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );
25 }
26
27 /**
28 * Check if the given IP address is an IPv6 address.
29 *
30 * @param string $ip The IP address to check.
31 *
32 * @return bool Returns true if the IP address is an IPv6 address, false otherwise.
33 */
34 private function is_v6( $ip ) {
35 return false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
36 }
37
38 /**
39 * Check if IPv6 is supported.
40 *
41 * @return bool
42 */
43 private function is_v6_support(): bool {
44 return defined( 'AF_INET6' );
45 }
46
47 /**
48 * Convert IPv4 address to IPv6 address.
49 *
50 * @param string $ip IPv4 address.
51 *
52 * @return bool|string
53 */
54 private function expand_ip_v6( $ip ) {
55 $hex = unpack( 'H*hex', inet_pton( $ip ) );
56
57 return substr( preg_replace( '/([A-f0-9]{4})/', '$1:', $hex['hex'] ), 0, - 1 );
58 }
59
60 /**
61 * Convert IPv6 address to binary.
62 *
63 * @param string $inet The packed data.
64 * @src https://stackoverflow.com/a/7951507
65 *
66 * @return string
67 */
68 private function ine_to_bits( $inet ): string {
69 $unpacked = unpack( 'a16', $inet );
70 $unpacked = str_split( $unpacked[1] );
71 $binaryip = '';
72 foreach ( $unpacked as $char ) {
73 $binaryip .= str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
74 }
75
76 return $binaryip;
77 }
78
79 /**
80 * Compare if an IPv4 address is within a specified range.
81 *
82 * @param string $ip The IPv4 address to compare.
83 * @param string $first_in_range The lower bound of the range.
84 * @param string $last_in_range The upper bound of the range.
85 *
86 * @return bool Returns true if the IP address is within the range, false otherwise.
87 */
88 private function compare_v4_in_range( $ip, $first_in_range, $last_in_range ): bool {
89 $low = sprintf( '%u', ip2long( $first_in_range ) );
90 $high = sprintf( '%u', ip2long( $last_in_range ) );
91
92 $cip = sprintf( '%u', ip2long( $ip ) );
93 if ( $high >= $cip && $cip >= $low ) {
94 return true;
95 }
96
97 return false;
98 }
99
100 /**
101 * Compare if an IPv6 address is within a specified range.
102 *
103 * @param string $ip The IPv6 address to compare.
104 * @param string $first_in_range The lower bound of the range.
105 * @param string $last_in_range The upper bound of the range.
106 *
107 * @return bool Returns true if the IP address is within the range, false otherwise.
108 */
109 private function compare_v6_in_range( $ip, $first_in_range, $last_in_range ): bool {
110 $first_in_range = inet_pton( $this->expand_ip_v6( $first_in_range ) );
111 $last_in_range = inet_pton( $this->expand_ip_v6( $last_in_range ) );
112 $ip = inet_pton( $this->expand_ip_v6( $ip ) );
113
114 if ( ( strlen( $ip ) === strlen( $first_in_range ) )
115 && ( $ip >= $first_in_range && $ip <= $last_in_range ) ) {
116 return true;
117 } else {
118 return false;
119 }
120 }
121
122 /**
123 * Compares an IPv4 address with a CIDR block.
124 *
125 * @param string $ip The IPv4 address to compare.
126 * @param string $block The CIDR block to compare against.
127 *
128 * @return bool Returns true if the IP address is within the CIDR block, false otherwise.
129 */
130 private function compare_cidrv4( $ip, $block ): bool {
131 [ $subnet, $bits ] = explode( '/', $block );
132 if ( is_null( $bits ) ) {
133 $bits = 32;
134 }
135 $ip = ip2long( $ip );
136 $subnet = ip2long( $subnet );
137 $mask = - 1 << ( 32 - (int) $bits );
138 $subnet &= $mask;// nb: in case the supplied subnet wasn't correctly aligned.
139
140 return ( $ip & $mask ) === $subnet;
141 }
142
143 /**
144 * Compares an IPv6 address with a CIDR block.
145 *
146 * @param string $ip The IPv6 address to compare.
147 * @param string $block The CIDR block to compare against.
148 *
149 * @return bool Returns true if the IP address is within the CIDR block, false otherwise.
150 */
151 private function compare_cidrv6( $ip, $block ): bool {
152 $ip = $this->expand_ip_v6( $ip );
153 $ip = inet_pton( $ip );
154 $b_ip = $this->ine_to_bits( $ip );
155 [ $subnet, $bits ] = explode( '/', $block );
156 $subnet = $this->expand_ip_v6( $subnet );
157 $subnet = inet_pton( $subnet );
158 $b_subnet = $this->ine_to_bits( $subnet );
159
160 $ip_net_bits = substr( $b_ip, 0, (int) $bits );
161 $subnet_bits = substr( $b_subnet, 0, (int) $bits );
162
163 return $ip_net_bits === $subnet_bits;
164 }
165
166 /**
167 * Compare ip2 to ip1, true if ip2>ip1, false if not.
168 *
169 * @param string $ip1 The first IP address to compare.
170 * @param string $ip2 The second IP address to compare.
171 *
172 * @return bool Returns true if ip2 is greater than ip1, false otherwise.
173 */
174 public function compare_ip( $ip1, $ip2 ): bool {
175 if ( $this->is_v4( $ip1 ) && $this->is_v4( $ip2 ) ) {
176 if ( sprintf( '%u', ip2long( $ip2 ) ) - sprintf( '%u', ip2long( $ip1 ) ) > 0 ) {
177 return true;
178 }
179 } elseif ( $this->is_v6( $ip1 ) && $this->is_v6( $ip2 ) && $this->is_v6_support() ) {
180 $ip1 = inet_pton( $this->expand_ip_v6( $ip1 ) );
181 $ip2 = inet_pton( $this->expand_ip_v6( $ip2 ) );
182
183 return $ip2 > $ip1;
184 }
185
186 return false;
187 }
188
189 /**
190 * Compare if an IP address is within a specified range.
191 *
192 * @param string $ip The IP address to compare.
193 * @param string $first_in_range The lower bound of the range.
194 * @param string $last_in_range The upper bound of the range.
195 *
196 * @return bool Returns true if the IP address is within the range, false otherwise.
197 */
198 public function compare_in_range( $ip, $first_in_range, $last_in_range ): bool {
199 if ( $this->is_v4( $first_in_range ) && $this->is_v4( $last_in_range ) ) {
200 return $this->compare_v4_in_range( $ip, $first_in_range, $last_in_range );
201 } elseif ( $this->is_v6( $first_in_range ) && $this->is_v6( $last_in_range ) && $this->is_v6_support() ) {
202 return $this->compare_v6_in_range( $ip, $first_in_range, $last_in_range );
203 }
204
205 return false;
206 }
207
208 /**
209 * Compares an IP address with a CIDR block.
210 *
211 * @param string $ip The IP address to compare.
212 * @param string $block The CIDR block to compare against.
213 *
214 * @return bool Returns true if the IP address is within the CIDR block, false otherwise.
215 */
216 public function compare_cidr( $ip, $block ): bool {
217 [ $subnet, $bits ] = explode( '/', $block );
218 if ( $this->is_v4( $ip ) && $this->is_v4( $subnet ) ) {
219 return $this->compare_cidrv4( $ip, $block );
220 } elseif ( $this->is_v6( $ip ) && $this->is_v6( $subnet ) && $this->is_v6_support() ) {
221 return $this->compare_cidrv6( $ip, $block );
222 }
223
224 return false;
225 }
226
227 /**
228 * $ip an be single ip, or a range like xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx or CIDR.
229 *
230 * @param string $ip The IP address to validate.
231 *
232 * @return bool
233 */
234 public function validate_ip( $ip ): bool {
235 if (
236 ! stristr( $ip, '-' )
237 && ! stristr( $ip, '/' )
238 && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
239 // Only ip, no '-', '/' symbols.
240 return true;
241 } elseif ( stristr( $ip, '-' ) ) {
242 $ips = explode( '-', $ip );
243 foreach ( $ips as $ip_key ) {
244 if ( ! filter_var( $ip_key, FILTER_VALIDATE_IP ) ) {
245 return false;
246 }
247 }
248 if ( $this->compare_ip( $ips[0], $ips[1] ) ) {
249 return true;
250 }
251 } elseif ( stristr( $ip, '/' ) ) {
252 [ $ip, $bits ] = explode( '/', $ip );
253 if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
254 if ( $this->is_v4( $ip ) && 0 <= $bits && $bits <= 32 ) {
255 return true;
256 } elseif ( $this->is_v6( $ip ) && 0 <= $bits && $bits <= 128 && $this->is_v6_support() ) {
257 return true;
258 }
259 }
260 }
261
262 return false;
263 }
264
265 /**
266 * Display a message if IP is non-valid. The cases:
267 * 1) single IP (no '-', '/' symbols),
268 * 2) IP range,
269 * 3) CIDR.
270 * Ignore cases with private, reserved ranges.
271 *
272 * @src https://en.wikipedia.org/wiki/IPv4#Special-use_addresses
273 * @src https://en.wikipedia.org/wiki/IPv6#Special-use_addresses
274 *
275 * @param mixed $ip IP address.
276 *
277 * @return array
278 */
279 public function display_validation_message( $ip ): array {
280 $errors = array();
281 // Case 1: single IP.
282 if (
283 ! stristr( $ip, '-' )
284 && ! stristr( $ip, '/' )
285 && ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
286 $errors[] = sprintf(
287 /* translators: %s: IP value. */
288 esc_html__( '%s – invalid format', 'defender-security' ),
289 '<b>' . $ip . '</b>'
290 );
291 // Case 2: IP range.
292 } elseif ( stristr( $ip, '-' ) ) {
293 $ips = explode( '-', $ip );
294 foreach ( $ips as $ip_key ) {
295 if ( ! filter_var( $ip_key, FILTER_VALIDATE_IP ) ) {
296 $errors[] = sprintf(
297 /* translators: %s: IP value. */
298 esc_html__( '%s – invalid format', 'defender-security' ),
299 '<b>' . $ip_key . '</b>'
300 );
301 }
302 }
303 if ( ! $this->compare_ip( $ips[0], $ips[1] ) ) {
304 $errors[] = sprintf(
305 /* translators: 1. IP value. 2. IP value. */
306 esc_html__( 'Can\'t compare %1$s with %2$s.', 'defender-security' ),
307 '<b>' . $ips[1] . '</b>',
308 '<b>' . $ips[0] . '</b>'
309 );
310 }
311 // Case 3: CIDR.
312 } elseif ( stristr( $ip, '/' ) ) {
313 [ $ip, $bits ] = explode( '/', $ip );
314 if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
315 if ( ! $this->is_v4( $ip ) || 0 > $bits || $bits > 32 ) {
316 if ( ! $this->is_v6( $ip ) || 0 > $bits || $bits > 128 || ! $this->is_v6_support() ) {
317 $errors[] = sprintf(
318 /* translators: %s: IP value. */
319 esc_html__( '%s – address out of range', 'defender-security' ),
320 '<b>' . $ip . '</b>'
321 );
322 }
323 }
324 } else {
325 $errors[] = sprintf(
326 /* translators: %s: IP value. */
327 esc_html__( '%s – invalid format', 'defender-security' ),
328 '<b>' . $ip . '</b>'
329 );
330 }
331 }
332
333 // @since 2.6.3
334 $errors = apply_filters( 'wd_display_ip_validations', $errors );
335 return is_array( $errors ) ? $errors : (array) $errors;
336 }
337
338 /**
339 * Validate IP.
340 *
341 * @param mixed $ip IP address.
342 *
343 * @return bool
344 */
345 public function check_validate_ip( $ip ): bool {
346 // Validate the localhost IP address.
347 if ( in_array( $ip, $this->get_localhost_ips(), true ) ) {
348 return true;
349 }
350
351 $filter_flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
352 // @since 2.4.7
353 if ( apply_filters( 'wp_defender_filtered_internal_ip', false ) ) {
354 // Todo: improve display of IP log when filtering reserved or private IPv4/IPv6 ranges.
355 $filter_flags = $filter_flags | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
356 }
357
358 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $filter_flags ) ) {
359 return false;
360 }
361
362 return true;
363 }
364
365 /**
366 * Get user IP.
367 *
368 * @return array
369 */
370 public function get_user_ip(): array {
371 $service = wd_di()->get( Smart_Ip_Detection::class );
372 if ( $service->is_smart_ip_detection_enabled() ) {
373 $ip_detail = $service->get_smart_ip_detection_details();
374 $ips = isset( $ip_detail[0] ) ? array( $ip_detail[0] ) : array();
375 } else {
376 $remote_addr = wd_di()->get( Remote_Address::class );
377 $ips = (array) $remote_addr->get_ip_address();
378 }
379
380 return $this->filter_user_ips( $ips );
381 }
382
383 /**
384 * Get user IP header.
385 *
386 * @return string
387 */
388 public function get_user_ip_header(): string {
389 $ip_header = '';
390
391 $service = wd_di()->get( Smart_Ip_Detection::class );
392 if ( $service->is_smart_ip_detection_enabled() ) {
393 $ip_detail = $service->get_smart_ip_detection_details();
394 $ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
395 } else {
396 $model = wd_di()->get( Firewall::class );
397 $remote_addr = wd_di()->get( Remote_Address::class );
398 $ip_header = $remote_addr->get_http_ip_header_value( esc_html( $model->http_ip_header ) );
399 }
400
401 return $ip_header;
402 }
403
404 /**
405 * Checks if an IP address is in the correct format within a given array of IP addresses.
406 *
407 * @param string $ip The IP address to check.
408 * @param array $arr_ips An array of IP addresses to check against.
409 *
410 * @return bool Returns true if the IP address is in the correct format within the array, false otherwise.
411 */
412 public function is_ip_in_format( $ip, $arr_ips ): bool {
413 foreach ( $arr_ips as $wip ) {
414 if ( false === strpos( $wip, '-' ) && false === strpos( $wip, '/' ) && trim( $wip ) === $ip ) {
415 return true;
416 } elseif ( false !== strpos( $wip, '-' ) ) {
417 $ips = explode( '-', $wip );
418 if ( $this->compare_in_range( $ip, $ips[0], $ips[1] ) ) {
419 return true;
420 }
421 } elseif ( false !== strpos( $wip, '/' ) && $this->compare_cidr( $ip, $wip ) ) {
422 return true;
423 }
424 }
425
426 return false;
427 }
428
429 /**
430 * Filter user IPs.
431 * This function takes an array of user IPs, applies the 'defender_user_ip'
432 * filter to each IP, and returns a unique array of filtered values.
433 *
434 * @param array $ips An array of user IPs.
435 *
436 * @return array An array of unique, filtered user IPs.
437 * @since 4.4.2
438 */
439 public function filter_user_ips( array $ips ): array {
440 $ips_filtered = array();
441 foreach ( $ips as $ip ) {
442 /**
443 * Filters the user IP.
444 *
445 * @param string $ip The user IP.
446 */
447 $ips_filtered[] = apply_filters( 'defender_user_ip', $ip );
448 }
449
450 return array_unique( $ips_filtered );
451 }
452
453 /**
454 * Use $_SERVER['REMOTE_ADDR'] as the first protection layer to avoid spoofed headers.
455 *
456 * @param string $blocked_ip The IP address to check.
457 *
458 * @return string
459 */
460 public function check_ip_by_remote_addr( $blocked_ip ): string {
461 $ip_addr = defender_get_data_from_request( 'REMOTE_ADDR', 's' );
462
463 return '' !== $ip_addr ? $ip_addr : $blocked_ip;
464 }
465
466 /**
467 * Check if the given IP is a private IP.
468 *
469 * @param string $ip The IP address to check.
470 *
471 * @return bool True if the IP is a private IP, false otherwise.
472 */
473 public function is_private_ip( string $ip ): bool {
474 return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE ) === false;
475 }
476
477 /**
478 * Maybe Loopback?
479 * Don't check '127.0.0.0/8' and '::1/128'.
480 *
481 * @return array
482 */
483 public function get_localhost_ips(): array {
484 return array( '127.0.0.1', '::1' );
485 }
486
487 /**
488 * Determines if the current request originates from this server.
489 *
490 * Validates that the request is a loopback from the server by:
491 * 1. Matching the IP against localhost, hostname IP, or a whitelisted server public IP.
492 * 2. Matching the User-Agent against the WordPress core loopback format.
493 *
494 * @return bool True if it's a valid server-originated loopback request; false otherwise.
495 */
496 public function request_is_from_server(): bool {
497 $user_ips = $this->get_user_ip();
498 if ( array() === $user_ips ) {
499 return false;
500 }
501
502 // Start building list of trusted server IPs.
503 $trusted_ips = $this->get_localhost_ips();
504 $server_ip = defender_get_data_from_request( 'SERVER_ADDR', 's' );
505 if ( $this->check_validate_ip( $server_ip ) ) {
506 $trusted_ips[] = $server_ip;
507 }
508
509 $stored_ip = get_site_option( \WP_Defender\Component\Firewall::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' );
510 if ( $this->check_validate_ip( $stored_ip ) ) {
511 $trusted_ips[] = $stored_ip;
512 }
513
514 /**
515 * Filters the list of trusted server IP addresses used to validate server-originated loopback requests.
516 *
517 * This filter allows developers to customize the list of IP addresses considered as trusted sources
518 * for internal server requests. These IPs are checked against the request's origin IP to determine
519 * whether it's a valid server-initiated loopback (e.g., WordPress cron or REST loopback check).
520 *
521 * Common use cases:
522 * - Add public-facing server IPs behind proxies/load balancers.
523 * - Adjust loopback behavior in containerized or cloud environments.
524 * - Remove/override default server hostname resolution or localhost IPs.
525 *
526 * @since 5.3.0
527 *
528 * @param array $trusted_ips An array of trusted server IP addresses.
529 */
530 $trusted_ips = apply_filters( 'wp_defender_server_ips', array_unique( $trusted_ips ) );
531 $trusted_ips = ! is_array( $trusted_ips ) ? (array) $trusted_ips : $trusted_ips;
532 // Only return true if the request IP is in the trusted list.
533 if ( array() !== array_intersect( $user_ips, $trusted_ips ) ) {
534 return true;
535 }
536
537 // User-Agent missing? Not a valid loopback.
538 $server_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
539 if ( '' === $server_agent ) {
540 return false;
541 }
542
543 // User-Agent must match WordPress loopback format.
544 $expected_ua = 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' );
545
546 return $server_agent === $expected_ua;
547 }
548 }
549