PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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 5.7.2, at src/traits/ip.php

547 lines 16.2 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 return (array) apply_filters( 'wd_display_ip_validations', $errors );
335 }
336
337 /**
338 * Validate IP.
339 *
340 * @param mixed $ip IP address.
341 *
342 * @return bool
343 */
344 public function check_validate_ip( $ip ): bool {
345 // Validate the localhost IP address.
346 if ( in_array( $ip, $this->get_localhost_ips(), true ) ) {
347 return true;
348 }
349
350 $filter_flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
351 // @since 2.4.7
352 if ( apply_filters( 'wp_defender_filtered_internal_ip', false ) ) {
353 // Todo: improve display of IP log when filtering reserved or private IPv4/IPv6 ranges.
354 $filter_flags = $filter_flags | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
355 }
356
357 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $filter_flags ) ) {
358 return false;
359 }
360
361 return true;
362 }
363
364 /**
365 * Get user IP.
366 *
367 * @return array
368 */
369 public function get_user_ip(): array {
370 $service = wd_di()->get( Smart_Ip_Detection::class );
371 if ( $service->is_smart_ip_detection_enabled() ) {
372 $ip_detail = $service->get_smart_ip_detection_details();
373 $ips = isset( $ip_detail[0] ) ? array( $ip_detail[0] ) : array();
374 } else {
375 $remote_addr = wd_di()->get( Remote_Address::class );
376 $ips = (array) $remote_addr->get_ip_address();
377 }
378
379 return $this->filter_user_ips( $ips );
380 }
381
382 /**
383 * Get user IP header.
384 *
385 * @return string
386 */
387 public function get_user_ip_header(): string {
388 $ip_header = '';
389
390 $service = wd_di()->get( Smart_Ip_Detection::class );
391 if ( $service->is_smart_ip_detection_enabled() ) {
392 $ip_detail = $service->get_smart_ip_detection_details();
393 $ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
394 } else {
395 $model = wd_di()->get( Firewall::class );
396 $remote_addr = wd_di()->get( Remote_Address::class );
397 $ip_header = $remote_addr->get_http_ip_header_value( esc_html( $model->http_ip_header ) );
398 }
399
400 return $ip_header;
401 }
402
403 /**
404 * Checks if an IP address is in the correct format within a given array of IP addresses.
405 *
406 * @param string $ip The IP address to check.
407 * @param array $arr_ips An array of IP addresses to check against.
408 *
409 * @return bool Returns true if the IP address is in the correct format within the array, false otherwise.
410 */
411 public function is_ip_in_format( $ip, $arr_ips ): bool {
412 foreach ( $arr_ips as $wip ) {
413 if ( false === strpos( $wip, '-' ) && false === strpos( $wip, '/' ) && trim( $wip ) === $ip ) {
414 return true;
415 } elseif ( false !== strpos( $wip, '-' ) ) {
416 $ips = explode( '-', $wip );
417 if ( $this->compare_in_range( $ip, $ips[0], $ips[1] ) ) {
418 return true;
419 }
420 } elseif ( false !== strpos( $wip, '/' ) && $this->compare_cidr( $ip, $wip ) ) {
421 return true;
422 }
423 }
424
425 return false;
426 }
427
428 /**
429 * Filter user IPs.
430 * This function takes an array of user IPs, applies the 'defender_user_ip'
431 * filter to each IP, and returns a unique array of filtered values.
432 *
433 * @param array $ips An array of user IPs.
434 *
435 * @return array An array of unique, filtered user IPs.
436 * @since 4.4.2
437 */
438 public function filter_user_ips( array $ips ): array {
439 $ips_filtered = array();
440 foreach ( $ips as $ip ) {
441 /**
442 * Filters the user IP.
443 *
444 * @param string $ip The user IP.
445 */
446 $ips_filtered[] = apply_filters( 'defender_user_ip', $ip );
447 }
448
449 return array_unique( $ips_filtered );
450 }
451
452 /**
453 * Use $_SERVER['REMOTE_ADDR'] as the first protection layer to avoid spoofed headers.
454 *
455 * @param string $blocked_ip The IP address to check.
456 *
457 * @return string
458 */
459 public function check_ip_by_remote_addr( $blocked_ip ): string {
460 $ip_addr = defender_get_data_from_request( 'REMOTE_ADDR', 's' );
461
462 return '' !== $ip_addr ? $ip_addr : $blocked_ip;
463 }
464
465 /**
466 * Check if the given IP is a private IP.
467 *
468 * @param string $ip The IP address to check.
469 *
470 * @return bool True if the IP is a private IP, false otherwise.
471 */
472 public function is_private_ip( string $ip ): bool {
473 return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE ) === false;
474 }
475
476 /**
477 * Maybe Loopback?
478 * Don't check '127.0.0.0/8' and '::1/128'.
479 *
480 * @return array
481 */
482 public function get_localhost_ips(): array {
483 return array( '127.0.0.1', '::1' );
484 }
485
486 /**
487 * Determines if the current request originates from this server.
488 *
489 * Validates that the request is a loopback from the server by:
490 * 1. Matching the IP against localhost, hostname IP, or a whitelisted server public IP.
491 * 2. Matching the User-Agent against the WordPress core loopback format.
492 *
493 * @return bool True if it's a valid server-originated loopback request; false otherwise.
494 */
495 public function request_is_from_server(): bool {
496 $user_ips = $this->get_user_ip();
497 if ( array() === $user_ips ) {
498 return false;
499 }
500
501 // Start building list of trusted server IPs.
502 $trusted_ips = $this->get_localhost_ips();
503 $server_ip = defender_get_data_from_request( 'SERVER_ADDR', 's' );
504 if ( $this->check_validate_ip( $server_ip ) ) {
505 $trusted_ips[] = $server_ip;
506 }
507
508 $stored_ip = get_site_option( \WP_Defender\Component\Firewall::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' );
509 if ( $this->check_validate_ip( $stored_ip ) ) {
510 $trusted_ips[] = $stored_ip;
511 }
512
513 /**
514 * Filters the list of trusted server IP addresses used to validate server-originated loopback requests.
515 *
516 * This filter allows developers to customize the list of IP addresses considered as trusted sources
517 * for internal server requests. These IPs are checked against the request's origin IP to determine
518 * whether it's a valid server-initiated loopback (e.g., WordPress cron or REST loopback check).
519 *
520 * Common use cases:
521 * - Add public-facing server IPs behind proxies/load balancers.
522 * - Adjust loopback behavior in containerized or cloud environments.
523 * - Remove/override default server hostname resolution or localhost IPs.
524 *
525 * @since 5.3.0
526 *
527 * @param array $trusted_ips An array of trusted server IP addresses.
528 */
529 $trusted_ips = (array) apply_filters( 'wp_defender_server_ips', array_unique( $trusted_ips ) );
530 // Only return true if the request IP is in the trusted list.
531 if ( array() !== array_intersect( $user_ips, $trusted_ips ) ) {
532 return true;
533 }
534
535 // User-Agent missing? Not a valid loopback.
536 $server_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
537 if ( '' === $server_agent ) {
538 return false;
539 }
540
541 // User-Agent must match WordPress loopback format.
542 $expected_ua = 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' );
543
544 return $server_agent === $expected_ua;
545 }
546 }
547