PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/traits/ip.php +292 -218 3.0.1 → 6.3.0 View file →
@@ -1,55 +1,72 @@
1 1 <?php
2 +/**
3 + * Helper functions for IP related tasks.
4 + *
5 + * @package WP_Defender\Traits
6 + */
2 7
3 8 namespace WP_Defender\Traits;
4 9
5 -use WP_Defender\Extra\IP_Helper;
10 +use WP_Defender\Component\Http\Remote_Address;
11 +use WP_Defender\Component\Smart_Ip_Detection;
12 +use WP_Defender\Model\Setting\Firewall;
6 13
7 14 trait IP {
15 +
8 16 /**
9 - * @param $ip
17 + * Check if the IP is IPv4 address.
10 18 *
11 - * @return mixed
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.
12 22 */
13 23 private function is_v4( $ip ) {
14 - return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );
24 + return false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );
15 25 }
16 26
17 27 /**
18 - * @param $ip
28 + * Check if the given IP address is an IPv6 address.
19 29 *
20 - * @return mixed
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.
21 33 */
22 34 private function is_v6( $ip ) {
23 - return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
35 + return false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
24 36 }
25 37
26 38 /**
39 + * Check if IPv6 is supported.
40 + *
27 41 * @return bool
28 42 */
29 - private function is_v6_support() {
43 + private function is_v6_support(): bool {
30 44 return defined( 'AF_INET6' );
31 45 }
32 46
33 47 /**
34 - * @param $ip
48 + * Convert IPv4 address to IPv6 address.
35 49 *
50 + * @param string $ip IPv4 address.
51 + *
36 52 * @return bool|string
37 53 */
38 54 private function expand_ip_v6( $ip ) {
39 55 $hex = unpack( 'H*hex', inet_pton( $ip ) );
40 - $ip = substr( preg_replace( '/([A-f0-9]{4})/', '$1:', $hex['hex'] ), 0, - 1 );
41 56
42 - return $ip;
57 + return substr( preg_replace( '/([A-f0-9]{4})/', '$1:', $hex['hex'] ), 0, - 1 );
43 58 }
44 59
45 60 /**
46 - * @param $inet
61 + * Convert IPv6 address to binary.
47 62 *
63 + * @param string $inet The packed data.
48 64 * @src https://stackoverflow.com/a/7951507
65 + *
49 66 * @return string
50 67 */
51 - private function ine_to_bits( $inet ) {
68 + private function ine_to_bits( $inet ): string {
52 69 $unpacked = unpack( 'a16', $inet );
53 70 $unpacked = str_split( $unpacked[1] );
54 71 $binaryip = '';
55 72 foreach ( $unpacked as $char ) {
@@ -59,15 +76,17 @@
59 76 return $binaryip;
60 77 }
61 78
62 79 /**
63 - * @param $ip
64 - * @param $first_in_range
65 - * @param $last_in_range
80 + * Compare if an IPv4 address is within a specified range.
66 81 *
67 - * @return bool
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.
68 87 */
69 - private function compare_v4_in_range( $ip, $first_in_range, $last_in_range ) {
88 + private function compare_v4_in_range( $ip, $first_in_range, $last_in_range ): bool {
70 89 $low = sprintf( '%u', ip2long( $first_in_range ) );
71 90 $high = sprintf( '%u', ip2long( $last_in_range ) );
72 91
73 92 $cip = sprintf( '%u', ip2long( $ip ) );
@@ -78,15 +97,17 @@
78 97 return false;
79 98 }
80 99
81 100 /**
82 - * @param $ip
83 - * @param $first_in_range
84 - * @param $last_in_range
101 + * Compare if an IPv6 address is within a specified range.
85 102 *
86 - * @return bool
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.
87 108 */
88 - private function compare_v6_in_range( $ip, $first_in_range, $last_in_range ) {
109 + private function compare_v6_in_range( $ip, $first_in_range, $last_in_range ): bool {
89 110 $first_in_range = inet_pton( $this->expand_ip_v6( $first_in_range ) );
90 111 $last_in_range = inet_pton( $this->expand_ip_v6( $last_in_range ) );
91 112 $ip = inet_pton( $this->expand_ip_v6( $ip ) );
92 113
@@ -98,44 +119,47 @@
98 119 }
99 120 }
100 121
101 122 /**
102 - * @param $ip
103 - * @param $block
123 + * Compares an IPv4 address with a CIDR block.
104 124 *
105 - * @src http://stackoverflow.com/a/594134
106 - * @return bool
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.
107 129 */
108 - private function compare_cidrv4( $ip, $block ) {
109 - [$subnet, $bits] = explode( '/', $block );
130 + private function compare_cidrv4( $ip, $block ): bool {
131 + [ $subnet, $bits ] = explode( '/', $block );
110 132 if ( is_null( $bits ) ) {
111 133 $bits = 32;
112 134 }
113 135 $ip = ip2long( $ip );
114 136 $subnet = ip2long( $subnet );
115 - $mask = - 1 << ( 32 - $bits );
116 - $subnet &= $mask;// nb: in case the supplied subnet wasn't correctly aligned
137 + $mask = - 1 << ( 32 - (int) $bits );
138 + $subnet &= $mask;// nb: in case the supplied subnet wasn't correctly aligned.
117 139
118 - return ( $ip & $mask ) == $subnet;// phpcs:ignore
140 + return ( $ip & $mask ) === $subnet;
119 141 }
120 142
121 143 /**
122 - * @param $ip
123 - * @param $block
144 + * Compares an IPv6 address with a CIDR block.
124 145 *
125 - * @return bool
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.
126 150 */
127 - private function compare_cidrv6( $ip, $block ) {
128 - $ip = $this->expand_ip_v6( $ip );
129 - $ip = inet_pton( $ip );
130 - $b_ip = $this->ine_to_bits( $ip );
131 - [$subnet, $bits] = explode( '/', $block );
132 - $subnet = $this->expand_ip_v6( $subnet );
133 - $subnet = inet_pton( $subnet );
134 - $b_subnet = $this->ine_to_bits( $subnet );
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 );
135 159
136 - $ip_net_bits = substr( $b_ip, 0, $bits );
137 - $subnet_bits = substr( $b_subnet, 0, $bits );
160 + $ip_net_bits = substr( $b_ip, 0, (int) $bits );
161 + $subnet_bits = substr( $b_subnet, 0, (int) $bits );
138 162
139 163 return $ip_net_bits === $subnet_bits;
140 164 }
141 165
@@ -141,14 +165,14 @@
141 165
142 166 /**
143 167 * Compare ip2 to ip1, true if ip2>ip1, false if not.
144 168 *
145 - * @param $ip1
146 - * @param $ip2
169 + * @param string $ip1 The first IP address to compare.
170 + * @param string $ip2 The second IP address to compare.
147 171 *
148 - * @return bool
172 + * @return bool Returns true if ip2 is greater than ip1, false otherwise.
149 173 */
150 - public function compare_ip( $ip1, $ip2 ) {
174 + public function compare_ip( $ip1, $ip2 ): bool {
151 175 if ( $this->is_v4( $ip1 ) && $this->is_v4( $ip2 ) ) {
152 176 if ( sprintf( '%u', ip2long( $ip2 ) ) - sprintf( '%u', ip2long( $ip1 ) ) > 0 ) {
153 177 return true;
154 178 }
@@ -162,26 +186,36 @@
162 186 return false;
163 187 }
164 188
165 189 /**
166 - * @param $ip
167 - * @param $first_in_range
168 - * @param $last_in_range
190 + * Compare if an IP address is within a specified range.
169 191 *
170 - * @return bool
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.
171 197 */
172 - public function compare_in_range( $ip, $first_in_range, $last_in_range ) {
198 + public function compare_in_range( $ip, $first_in_range, $last_in_range ): bool {
173 199 if ( $this->is_v4( $first_in_range ) && $this->is_v4( $last_in_range ) ) {
174 200 return $this->compare_v4_in_range( $ip, $first_in_range, $last_in_range );
175 201 } elseif ( $this->is_v6( $first_in_range ) && $this->is_v6( $last_in_range ) && $this->is_v6_support() ) {
176 - $this->compare_v6_in_range( $ip, $first_in_range, $last_in_range );
202 + return $this->compare_v6_in_range( $ip, $first_in_range, $last_in_range );
177 203 }
178 204
179 205 return false;
180 206 }
181 207
182 - public function compare_cidr( $ip, $block ) {
183 - [$subnet, $bits] = explode( '/', $block );
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 );
184 218 if ( $this->is_v4( $ip ) && $this->is_v4( $subnet ) ) {
185 219 return $this->compare_cidrv4( $ip, $block );
186 220 } elseif ( $this->is_v6( $ip ) && $this->is_v6( $subnet ) && $this->is_v6_support() ) {
187 221 return $this->compare_cidrv6( $ip, $block );
@@ -191,13 +225,14 @@
191 225 }
192 226
193 227 /**
194 228 * $ip an be single ip, or a range like xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx or CIDR.
195 - * @param $ip
196 229 *
230 + * @param string $ip The IP address to validate.
231 + *
197 232 * @return bool
198 233 */
199 - public function validate_ip( $ip ) {
234 + public function validate_ip( $ip ): bool {
200 235 if (
201 236 ! stristr( $ip, '-' )
202 237 && ! stristr( $ip, '/' )
203 238 && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
@@ -213,9 +248,9 @@
213 248 if ( $this->compare_ip( $ips[0], $ips[1] ) ) {
214 249 return true;
215 250 }
216 251 } elseif ( stristr( $ip, '/' ) ) {
217 - [$ip, $bits] = explode( '/', $ip );
252 + [ $ip, $bits ] = explode( '/', $ip );
218 253 if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
219 254 if ( $this->is_v4( $ip ) && 0 <= $bits && $bits <= 32 ) {
220 255 return true;
221 256 } elseif ( $this->is_v6( $ip ) && 0 <= $bits && $bits <= 128 && $this->is_v6_support() ) {
@@ -232,178 +267,161 @@
232 267 * 1) single IP (no '-', '/' symbols),
233 268 * 2) IP range,
234 269 * 3) CIDR.
235 270 * Ignore cases with private, reserved ranges.
271 + *
236 272 * @src https://en.wikipedia.org/wiki/IPv4#Special-use_addresses
237 273 * @src https://en.wikipedia.org/wiki/IPv6#Special-use_addresses
238 - * @param $ip
239 274 *
275 + * @param mixed $ip IP address.
276 + *
240 277 * @return array
241 - */
242 - public function display_validation_message( $ip ) {
278 + */
279 + public function display_validation_message( $ip ): array {
243 280 $errors = array();
244 - // Case1: single IP.
281 + // Case 1: single IP.
245 282 if (
246 283 ! stristr( $ip, '-' )
247 284 && ! stristr( $ip, '/' )
248 285 && ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
249 286 $errors[] = sprintf(
250 - /* translators: ... */
251 - __( '<b>%s</b> – invalid format', 'wpdef' ),
252 - $ip
287 + /* translators: %s: IP value. */
288 + esc_html__( '%s – invalid format', 'defender-security' ),
289 + '<b>' . $ip . '</b>'
253 290 );
254 - // Case2: IP range.
291 + // Case 2: IP range.
255 292 } elseif ( stristr( $ip, '-' ) ) {
256 293 $ips = explode( '-', $ip );
257 294 foreach ( $ips as $ip_key ) {
258 295 if ( ! filter_var( $ip_key, FILTER_VALIDATE_IP ) ) {
259 296 $errors[] = sprintf(
260 - /* translators: ... */
261 - __( '<b>%s</b> – invalid format', 'wpdef' ),
262 - $ip_key
297 + /* translators: %s: IP value. */
298 + esc_html__( '%s – invalid format', 'defender-security' ),
299 + '<b>' . $ip_key . '</b>'
263 300 );
264 301 }
265 302 }
266 303 if ( ! $this->compare_ip( $ips[0], $ips[1] ) ) {
267 304 $errors[] = sprintf(
268 - /* translators: ... */
269 - __( "Can't compare <b>%s</b> with <b>%s</b>.", 'wpdef' ),
270 - $ips[1],
271 - $ips[0]
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>'
272 309 );
273 310 }
274 - // Case3: CIDR.
311 + // Case 3: CIDR.
275 312 } elseif ( stristr( $ip, '/' ) ) {
276 - [$ip, $bits] = explode( '/', $ip );
313 + [ $ip, $bits ] = explode( '/', $ip );
277 314 if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
278 - if ( $this->is_v4( $ip ) && 0 <= $bits && $bits <= 32 ) {
279 - // IPv4 is correct.
280 - } elseif ( $this->is_v6( $ip ) && 0 <= $bits && $bits <= 128 && $this->is_v6_support() ) {
281 - // IPv6 is correct.
282 - } else {
283 - $errors[] = sprintf(
284 - /* translators: ... */
285 - __( '<b>%s</b> – address out of range', 'wpdef' ),
286 - $ip
287 - );
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 + }
288 323 }
289 324 } else {
290 325 $errors[] = sprintf(
291 - /* translators: ... */
292 - __( '<b>%s</b> – invalid format', 'wpdef' ),
293 - $ip
326 + /* translators: %s: IP value. */
327 + esc_html__( '%s – invalid format', 'defender-security' ),
328 + '<b>' . $ip . '</b>'
294 329 );
295 330 }
296 331 }
332 +
297 333 // @since 2.6.3
298 - return apply_filters( 'wd_display_ip_validations', $errors );
334 + $errors = apply_filters( 'wd_display_ip_validations', $errors );
335 + return is_array( $errors ) ? $errors : (array) $errors;
299 336 }
300 337
301 338 /**
302 - * Check if there are any cloudflare headers in the request.
339 + * Validate IP.
303 340 *
341 + * @param mixed $ip IP address.
342 + *
304 343 * @return bool
305 344 */
306 - private function is_cloudflare_request() {
307 - if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
345 + public function check_validate_ip( $ip ): bool {
346 + // Validate the localhost IP address.
347 + if ( in_array( $ip, $this->get_localhost_ips(), true ) ) {
308 348 return true;
309 349 }
310 - if ( isset( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) {
311 - return true;
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;
312 356 }
313 - if ( isset( $_SERVER['HTTP_CF_RAY'] ) ) {
314 - return true;
357 +
358 + if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $filter_flags ) ) {
359 + return false;
315 360 }
316 - if ( isset( $_SERVER['HTTP_CF_VISITOR'] ) ) {
317 - return true;
318 - }
319 361
320 - return false;
362 + return true;
321 363 }
322 364
323 365 /**
324 - * We fetch the ip range here.
325 - * https://www.cloudflare.com/ips/
366 + * Get user IP.
326 367 *
327 - * @since 2.5.0 Update the ip range.
328 - *
329 368 * @return array
330 369 */
331 - private function cloudflare_ip_ranges() {
332 - return array(
333 - array(
334 - '173.245.48.0/20',
335 - '103.21.244.0/22',
336 - '103.22.200.0/22',
337 - '103.31.4.0/22',
338 - '141.101.64.0/18',
339 - '108.162.192.0/18',
340 - '190.93.240.0/20',
341 - '188.114.96.0/20',
342 - '197.234.240.0/22',
343 - '198.41.128.0/17',
344 - '162.158.0.0/15',
345 - '172.64.0.0/13',
346 - '131.0.72.0/22',
347 - '104.16.0.0/13',
348 - '104.24.0.0/14',
349 - ),
350 - array(
351 - '2400:cb00::/32',
352 - '2606:4700::/32',
353 - '2803:f800::/32',
354 - '2405:b500::/32',
355 - '2405:8100::/32',
356 - '2a06:98c0::/29',
357 - '2c0f:f248::/32',
358 - ),
359 - );
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 );
360 381 }
361 382
362 383 /**
363 - * Check if the request is from cloudflare. If it is, we get the IP.
384 + * Get user IP header.
364 385 *
365 - * @return mixed
386 + * @return string
366 387 */
367 - private function cloudflare_ip() {
368 - if ( $this->is_cloudflare_request() ) {
369 - // This looks like it come from cloudflare, so this should contain the actual IP,
370 - // and REMOTE_ADDR contains cloudflare IP.
371 - [$cloudflare_ipv4_range, $cloudflare_ipv6_range] = $this->cloudflare_ip_ranges();
372 - $ip_helper = new IP_Helper();
373 - $remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) ?
374 - filter_var( $_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP ) :
375 - false;
376 - $cf_con_ip = isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ?
377 - filter_var( $_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP ) :
378 - false;
388 + public function get_user_ip_header(): string {
389 + $ip_header = '';
379 390
380 - if ( $remote_addr === $cf_con_ip ) {
381 - return $cf_con_ip;
382 - } else if ( false === $remote_addr || false === $cf_con_ip ) {
383 - return false;
384 - }
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 + }
385 400
386 - $is_cloudflare_proxy_ip = false;
387 - if ( $this->is_v4( $remote_addr ) ) {
388 - foreach ( $cloudflare_ipv4_range as $cf_ip ) {
389 - if ( $ip_helper->ipv4_in_range( $remote_addr, $cf_ip ) ) {
390 - $is_cloudflare_proxy_ip = true;
391 - break;
392 - }
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;
393 420 }
394 - } elseif ( $this->is_v6( $remote_addr ) ) {
395 - foreach ( $cloudflare_ipv6_range as $cf_ip ) {
396 - if ( $ip_helper->ipv6_in_range( $remote_addr, $cf_ip ) ) {
397 - $is_cloudflare_proxy_ip = true;
398 - break;
399 - }
400 - }
421 + } elseif ( false !== strpos( $wip, '/' ) && $this->compare_cidr( $ip, $wip ) ) {
422 + return true;
401 423 }
402 -
403 - if ( true === $is_cloudflare_proxy_ip ) {
404 - return $cf_con_ip;
405 - }
406 424 }
407 425
408 426 return false;
409 427 }
@@ -408,67 +426,123 @@
408 426 return false;
409 427 }
410 428
411 429 /**
412 - * Validate IP.
413 - * @param $ip
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.
414 433 *
415 - * @return bool
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
416 438 */
417 - public function check_validate_ip( $ip ) {
418 - // Validate the localhost IP address.
419 - if ( in_array( $ip, array( '127.0.0.1', '::1' ), true ) ) {
420 - return true;
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 );
421 448 }
422 449
423 - $filter_flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
424 - // @since 2.4.7
425 - if ( apply_filters( 'wp_defender_filtered_internal_ip', false ) ) {
426 - // Todo: improve display of IP log when filtering reserved or private IPv4/IPv6 ranges.
427 - $filter_flags = $filter_flags | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
428 - }
450 + return array_unique( $ips_filtered );
451 + }
429 452
430 - if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $filter_flags ) ) {
431 - return false;
432 - }
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' );
433 462
434 - return true;
463 + return '' !== $ip_addr ? $ip_addr : $blocked_ip;
435 464 }
436 465
437 466 /**
438 - * Get user IP.
467 + * Check if the given IP is a private IP.
439 468 *
440 - * @return string
469 + * @param string $ip The IP address to check.
470 + *
471 + * @return bool True if the IP is a private IP, false otherwise.
441 472 */
442 - public function get_user_ip() {
443 - // Check if it's any cloudflare IP.
444 - $cf_ip = $this->cloudflare_ip();
445 - if ( ! empty( $cf_ip ) && filter_var( $cf_ip, FILTER_VALIDATE_IP ) ) {
446 - return apply_filters( 'defender_user_ip', $cf_ip );
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;
447 500 }
448 501
449 - $headers = array(
450 - 'HTTP_CLIENT_IP',
451 - 'HTTP_X_REAL_IP',
452 - 'HTTP_X_FORWARDED_FOR',
453 - 'HTTP_X_FORWARDED',
454 - 'HTTP_X_CLUSTER_CLIENT_IP',
455 - 'HTTP_FORWARDED_FOR',
456 - 'HTTP_FORWARDED',
457 - 'REMOTE_ADDR',
458 - );
459 - $ip = '';
460 - foreach ( $headers as $key ) {
461 - if ( array_key_exists( $key, $_SERVER ) && ! empty( $_SERVER[ $key ] ) ) {
462 - $ip_array = explode( ',', $_SERVER[ $key ] );
463 - $tmp_ip = array_shift( $ip_array );
464 - $tmp_ip = trim( $tmp_ip );
465 - if ( $this->check_validate_ip( $tmp_ip ) ) {
466 - $ip = $tmp_ip;
467 - break;
468 - }
469 - }
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;
470 507 }
471 508
472 - return apply_filters( 'defender_user_ip', $ip );
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;
473 547 }
474 548 }