PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.10
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.10
2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 2.9.4 All 87 releases
vigilante / includes / class-ip-utils.php

class-ip-utils.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.11.10, at includes/class-ip-utils.php

872 lines 30.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * IP matching utilities
4 *
5 * Shared IP/pattern matching for the firewall and login modules. Supports
6 * exact addresses, CIDR ranges and wildcards, for both IPv4 and IPv6.
7 *
8 * @package Vigilante
9 */
10
11 // Prevent direct access
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Class Vigilante_IP_Utils
18 *
19 * Stateless helpers. All methods are static.
20 */
21 class Vigilante_IP_Utils {
22
23 /**
24 * Check whether an IP matches a single pattern.
25 *
26 * Supported pattern forms (IPv4 and IPv6 alike):
27 * - Exact address: 203.0.113.5 / 2a02:c207::1
28 * - CIDR range: 203.0.113.0/24 / 2a02:c207::/32
29 * - Wildcard: 203.0.113.* / 2a02:c207:*
30 *
31 * @param string $ip IP address to test.
32 * @param string $pattern Pattern to match against.
33 * @return bool True on match.
34 */
35 public static function matches( $ip, $pattern ) {
36 $ip = trim( (string) $ip );
37 $pattern = trim( (string) $pattern );
38
39 if ( '' === $ip || '' === $pattern ) {
40 return false;
41 }
42
43 // Exact match, comparing what the addresses ARE and not how they are
44 // written (see same_address()).
45 if ( self::same_address( $ip, $pattern ) ) {
46 return true;
47 }
48
49 // CIDR notation.
50 if ( false !== strpos( $pattern, '/' ) ) {
51 return self::cidr_match( $ip, $pattern );
52 }
53
54 // Wildcard notation.
55 if ( false !== strpos( $pattern, '*' ) ) {
56 return self::wildcard_match( $ip, $pattern );
57 }
58
59 return false;
60 }
61
62 /**
63 * Check whether an IP matches any pattern in a list.
64 *
65 * @param string $ip IP address to test.
66 * @param array $list List of patterns.
67 * @return bool True if any pattern matches.
68 */
69 public static function in_list( $ip, $list ) {
70 if ( empty( $list ) || ! is_array( $list ) ) {
71 return false;
72 }
73
74 foreach ( $list as $pattern ) {
75 if ( self::matches( $ip, (string) $pattern ) ) {
76 return true;
77 }
78 }
79
80 return false;
81 }
82
83 /**
84 * Whether an address is in a list, matching exact addresses and CIDR only.
85 *
86 * The strict cousin of in_list(), for deciding identity rather than
87 * filtering traffic. A wildcard entry (1.2.* or a bare *) is never honoured
88 * here: a proxy the site delegates its client IP to is a specific machine
89 * or a specific range, and a wildcard in that role is the trust-everyone
90 * footgun that would reopen the forwarded-header spoofing (6.1) this release
91 * closes, since matches() turns a bare * into /^.*$/ and trusts every peer.
92 * Kept apart from in_list() on purpose, so the firewall whitelist keeps its
93 * wildcards while the proxy-trust decision cannot grow one.
94 *
95 * @since 2.11.9
96 *
97 * @param string $ip Address to test.
98 * @param array $list List of exact addresses or CIDR ranges.
99 * @return bool
100 */
101 public static function in_list_ip_or_cidr( $ip, $list ) {
102 if ( empty( $list ) || ! is_array( $list ) ) {
103 return false;
104 }
105
106 $ip = trim( (string) $ip );
107 if ( '' === $ip ) {
108 return false;
109 }
110
111 foreach ( $list as $pattern ) {
112 $pattern = trim( (string) $pattern );
113
114 if ( '' === $pattern || false !== strpos( $pattern, '*' ) ) {
115 continue;
116 }
117
118 if ( self::same_address( $ip, $pattern ) ) {
119 return true;
120 }
121
122 if ( false !== strpos( $pattern, '/' ) ) {
123 // A range too wide to name anything is ignored here as well as
124 // rejected on the way in: an entry can arrive by import or from
125 // an older version, and it must not turn every peer into a
126 // trusted proxy. See proxy_prefix_is_sane().
127 if ( ! self::proxy_prefix_is_sane( $pattern ) ) {
128 continue;
129 }
130
131 if ( self::cidr_match( $ip, $pattern ) ) {
132 return true;
133 }
134 }
135 }
136
137 return false;
138 }
139
140 /**
141 * Whether a string is a pattern this class can actually match.
142 *
143 * The counterpart of matches(): everything this returns true for is
144 * something the matcher understands, and everything else is noise that
145 * would sit in an IP list looking effective while matching nothing. Kept
146 * next to the matcher on purpose, so validation and matching cannot drift
147 * apart again.
148 *
149 * @since 2.9.9
150 *
151 * @param string $pattern Candidate pattern.
152 * @return bool
153 */
154 public static function is_valid_pattern( $pattern ) {
155 $pattern = trim( (string) $pattern );
156
157 if ( '' === $pattern ) {
158 return false;
159 }
160
161 // Exact address, IPv4 or IPv6.
162 if ( filter_var( $pattern, FILTER_VALIDATE_IP ) ) {
163 return true;
164 }
165
166 // CIDR range: same criterion cidr_match() applies, family included.
167 if ( false !== strpos( $pattern, '/' ) ) {
168 $parts = explode( '/', $pattern, 2 );
169 if ( 2 !== count( $parts ) ) {
170 return false;
171 }
172
173 $subnet = trim( $parts[0] );
174 $bits = trim( $parts[1] );
175
176 if ( '' === $bits || ! ctype_digit( $bits ) ) {
177 return false;
178 }
179
180 if ( ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {
181 return false;
182 }
183
184 $packed = inet_pton( $subnet );
185 if ( false === $packed ) {
186 return false;
187 }
188
189 return ( (int) $bits <= strlen( $packed ) * 8 );
190 }
191
192 // Wildcard: what is left once the asterisks are gone has to be a
193 // plausible prefix, of one family only.
194 if ( false !== strpos( $pattern, '*' ) ) {
195 return self::is_valid_wildcard( $pattern );
196 }
197
198 return false;
199 }
200
201 /**
202 * Split a list into the patterns that can match and the ones that cannot.
203 *
204 * @since 2.9.9
205 *
206 * @param array|string $list List of patterns, or a newline separated string.
207 * @return array{valid: string[], rejected: string[]}
208 */
209 public static function split_list( $list ) {
210 if ( is_string( $list ) ) {
211 $list = preg_split( '/[\r\n]+/', $list );
212 }
213
214 $valid = array();
215 $rejected = array();
216
217 foreach ( (array) $list as $entry ) {
218 $entry = trim( (string) $entry );
219
220 if ( '' === $entry ) {
221 continue;
222 }
223
224 if ( self::is_valid_pattern( $entry ) ) {
225 $valid[] = $entry;
226 } else {
227 $rejected[] = $entry;
228 }
229 }
230
231 return array(
232 'valid' => array_values( array_unique( $valid ) ),
233 'rejected' => array_values( array_unique( $rejected ) ),
234 );
235 }
236
237 /**
238 * Whether a string is a proxy address this class trusts to set a header.
239 *
240 * A valid pattern that is not a wildcard: an exact address or a CIDR range.
241 * The counterpart of in_list_ip_or_cidr() for the save path, so a wildcard
242 * typed into the trusted proxies field is rejected with feedback instead of
243 * sitting there matching nothing (or, before the strict matcher, everything).
244 *
245 * @since 2.11.9
246 *
247 * @param string $pattern Candidate pattern.
248 * @return bool
249 */
250 public static function is_valid_proxy( $pattern ) {
251 $pattern = trim( (string) $pattern );
252
253 if ( false !== strpos( $pattern, '*' ) || ! self::is_valid_pattern( $pattern ) ) {
254 return false;
255 }
256
257 return self::proxy_prefix_is_sane( $pattern );
258 }
259
260 /**
261 * Whether a CIDR entry is narrow enough to name a proxy
262 *
263 * A prefix length of zero matches every address, so 0.0.0.0/0 and ::/0 say
264 * exactly what the rejected '*' says, written as a CIDR. Rejecting the
265 * wildcard and accepting those was the same footgun with another spelling:
266 * with either one in the list every peer counts as a trusted proxy and any
267 * visitor picks the address the firewall sees, which is the forwarded-header
268 * spoofing (6.1) this list exists to prevent. Found by the file-by-file
269 * review of 2.11.10.
270 *
271 * Stopping at zero was not enough, and that is the second cross review of
272 * 2.11.10: 0.0.0.0/1 and 128.0.0.0/1 are two accepted entries that between
273 * them cover the whole internet, with the same effect and no warning. So the
274 * question is not "is it zero" but "can this range name a proxy". The floors
275 * are 8 for IPv4, the widest range that still names something real (the
276 * classic private network is 10.0.0.0/8), and 7 for IPv6, because fc00::/7 is
277 * how the whole IPv6 private space is written and this very class treats it
278 * as the own network in is_own_network(). A first version put the IPv6 floor
279 * at 16 and refused fc00::/7, fd00::/8 (what Docker hands out) and fe80::/10,
280 * so a list that already held one of them stopped honouring the header
281 * altogether and every visitor came out with the proxy's address: the tool
282 * contradicting itself about what a private network is. Found by the third
283 * cross review of 2.11.10. With 7, ::/0 and 2000::/3, which is all of the
284 * routable internet, are still refused.
285 *
286 * Measured against what the CDNs publish, and all of them pass: Cloudflare
287 * (/13, /15, /29), Fastly (/16, /32), Akamai (/10, /11, /13, /24), Sucuri
288 * (/22, /23), Bunny (/32), CloudFront (/15) and Google (/16, /22).
289 *
290 * Kept deliberately as a floor and not as a warning: an entry this wide is
291 * indistinguishable from the wildcard that is already refused, and the cost
292 * of being wrong is that any visitor chooses their own address.
293 *
294 * @since 2.11.10
295 *
296 * @param string $pattern Address or CIDR range.
297 * @return bool True when it is an exact address or a narrow enough range.
298 */
299 public static function proxy_prefix_is_sane( $pattern ) {
300 $pattern = trim( (string) $pattern );
301
302 if ( false === strpos( $pattern, '/' ) ) {
303 return true;
304 }
305
306 $parts = explode( '/', $pattern, 2 );
307
308 if ( 2 !== count( $parts ) ) {
309 return false;
310 }
311
312 $subnet = trim( $parts[0] );
313 $bits = (int) trim( $parts[1] );
314 $packed = inet_pton( $subnet );
315
316 if ( false === $packed ) {
317 return false;
318 }
319
320 $minimo = ( 4 === strlen( $packed ) ) ? 8 : 7;
321
322 return ( $bits >= $minimo );
323 }
324
325 /**
326 * Whether two written addresses are the same address
327 *
328 * Comparing the strings was enough for IPv4 and wrong for IPv6, where the
329 * same address has many spellings: 2001:DB8::1, 2001:db8::1 and
330 * 2001:0db8:0000:0000:0000:0000:0000:0001 are one address written three
331 * ways, and only the last two compared equal to each other. It mattered
332 * because the .htaccess side normalises with inet_pton()/inet_ntop() before
333 * writing its rule, so Apache exempted a peer that PHP did not recognise,
334 * which is the direction that opens something: measured against a real
335 * Apache by the second cross review of 2.11.10.
336 *
337 * Falls back to the string comparison when either side is not an address, so
338 * nothing that used to match stops matching.
339 *
340 * @since 2.11.10
341 *
342 * @param string $a First address.
343 * @param string $b Second address.
344 * @return bool
345 */
346 public static function same_address( $a, $b ) {
347 if ( $a === $b ) {
348 return true;
349 }
350
351 $pa = inet_pton( $a );
352 $pb = inet_pton( $b );
353
354 if ( false === $pa || false === $pb ) {
355 return false;
356 }
357
358 return ( $pa === $pb );
359 }
360
361 /**
362 * Split a list into the proxy addresses that are valid and the ones that are not.
363 *
364 * Like split_list(), but rejecting wildcards: the trusted proxies list feeds
365 * an identity decision, and only exact addresses and CIDR ranges belong there.
366 *
367 * @since 2.11.9
368 *
369 * @param array|string $list List of patterns, or a newline separated string.
370 * @return array{valid: string[], rejected: string[]}
371 */
372 public static function split_list_ip_or_cidr( $list ) {
373 if ( is_string( $list ) ) {
374 $list = preg_split( '/[\r\n]+/', $list );
375 }
376
377 $valid = array();
378 $rejected = array();
379
380 foreach ( (array) $list as $entry ) {
381 $entry = trim( (string) $entry );
382
383 if ( '' === $entry ) {
384 continue;
385 }
386
387 if ( self::is_valid_proxy( $entry ) ) {
388 $valid[] = $entry;
389 } else {
390 $rejected[] = $entry;
391 }
392 }
393
394 return array(
395 'valid' => array_values( array_unique( $valid ) ),
396 'rejected' => array_values( array_unique( $rejected ) ),
397 );
398 }
399
400 /**
401 * Whether a wildcard pattern is plausible for one address family.
402 *
403 * A bare '*' is rejected on purpose: as a whitelist it would let everyone
404 * in and as a blacklist it would lock everyone out, and nobody types that
405 * meaning to.
406 *
407 * @since 2.9.9
408 *
409 * @param string $pattern Wildcard pattern.
410 * @return bool
411 */
412 private static function is_valid_wildcard( $pattern ) {
413 $bare = str_replace( '*', '', $pattern );
414
415 if ( '' === $bare || '.' === $bare || ':' === $bare ) {
416 return false;
417 }
418
419 // IPv6 when there is a colon, IPv4 otherwise. The two never mix.
420 if ( false !== strpos( $pattern, ':' ) ) {
421 if ( ! preg_match( '/^[0-9A-Fa-f:*]+$/', $pattern ) ) {
422 return false;
423 }
424
425 $groups = explode( ':', $pattern );
426 if ( count( $groups ) > 8 ) {
427 return false;
428 }
429
430 foreach ( $groups as $group ) {
431 if ( '' === $group || '*' === $group ) {
432 continue;
433 }
434 if ( ! preg_match( '/^[0-9A-Fa-f]{1,4}\*?$/', $group ) ) {
435 return false;
436 }
437 }
438
439 return true;
440 }
441
442 if ( ! preg_match( '/^[0-9.*]+$/', $pattern ) ) {
443 return false;
444 }
445
446 $octets = explode( '.', $pattern );
447 if ( count( $octets ) > 4 ) {
448 return false;
449 }
450
451 foreach ( $octets as $octet ) {
452 if ( '' === $octet || '*' === $octet ) {
453 continue;
454 }
455 // A partial octet such as 2* is a prefix, so it is not range checked.
456 if ( ! preg_match( '/^[0-9]{1,3}\*?$/', $octet ) ) {
457 return false;
458 }
459 if ( '*' !== substr( $octet, -1 ) && (int) $octet > 255 ) {
460 return false;
461 }
462 }
463
464 return true;
465 }
466
467 /**
468 * Match an IP against a CIDR range. Works for IPv4 and IPv6.
469 *
470 * The comparison is done on the packed binary form, so the textual
471 * representation of an IPv6 address (compressed or not) does not matter.
472 *
473 * @param string $ip IP address to test.
474 * @param string $cidr CIDR range (e.g. 203.0.113.0/24 or 2a02::/32).
475 * @return bool True on match.
476 */
477 private static function cidr_match( $ip, $cidr ) {
478 $parts = explode( '/', $cidr, 2 );
479 if ( 2 !== count( $parts ) ) {
480 return false;
481 }
482
483 $subnet = trim( $parts[0] );
484 $bits = trim( $parts[1] );
485
486 // Prefix length must be a plain integer.
487 if ( '' === $bits || ! ctype_digit( $bits ) ) {
488 return false;
489 }
490 $bits = (int) $bits;
491
492 // Validate both addresses before packing so inet_pton never warns.
493 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {
494 return false;
495 }
496
497 $ip_packed = inet_pton( $ip );
498 $subnet_packed = inet_pton( $subnet );
499 if ( false === $ip_packed || false === $subnet_packed ) {
500 return false;
501 }
502
503 // Different address family (4 bytes for IPv4, 16 for IPv6).
504 if ( strlen( $ip_packed ) !== strlen( $subnet_packed ) ) {
505 return false;
506 }
507
508 $max_bits = strlen( $ip_packed ) * 8;
509 if ( $bits < 0 || $bits > $max_bits ) {
510 return false;
511 }
512
513 // Compare whole bytes first.
514 $whole_bytes = intdiv( $bits, 8 );
515 if ( $whole_bytes > 0 && substr( $ip_packed, 0, $whole_bytes ) !== substr( $subnet_packed, 0, $whole_bytes ) ) {
516 return false;
517 }
518
519 // Then the remaining bits of the partial byte, if any.
520 $remaining = $bits % 8;
521 if ( $remaining > 0 ) {
522 $mask = 0xFF << ( 8 - $remaining ) & 0xFF;
523 $ip_byte = ord( $ip_packed[ $whole_bytes ] );
524 $sub_byte = ord( $subnet_packed[ $whole_bytes ] );
525 if ( ( $ip_byte & $mask ) !== ( $sub_byte & $mask ) ) {
526 return false;
527 }
528 }
529
530 return true;
531 }
532
533 /**
534 * Match an IP against a wildcard pattern (e.g. 203.0.113.* or 2a02:c207:*).
535 *
536 * Operates on the textual form. The '*' stands for any run of characters;
537 * every other character is matched literally, so it works for the dots of
538 * IPv4 and the colons of IPv6.
539 *
540 * @param string $ip IP address to test.
541 * @param string $pattern Wildcard pattern.
542 * @return bool True on match.
543 */
544 private static function wildcard_match( $ip, $pattern ) {
545 $quoted = preg_quote( $pattern, '/' );
546 $regex = '/^' . str_replace( '\*', '.*', $quoted ) . '$/';
547
548 return (bool) preg_match( $regex, $ip );
549 }
550
551 /**
552 * Proxy headers an admin may declare as trusted, mapped to their $_SERVER key.
553 *
554 * @return array<string,string>
555 */
556 public static function trusted_header_map() {
557 return array(
558 'cf-connecting-ip' => 'HTTP_CF_CONNECTING_IP',
559 'x-forwarded-for' => 'HTTP_X_FORWARDED_FOR',
560 'x-real-ip' => 'HTTP_X_REAL_IP',
561 );
562 }
563
564 /**
565 * The proxy header the admin has declared as trusted, or '' for none.
566 *
567 * @return string
568 */
569 public static function trusted_proxy_header() {
570 $options = get_option( 'vigilante_options' );
571 if ( is_array( $options ) && ! empty( $options['firewall']['trusted_proxy_header'] ) ) {
572 $header = (string) $options['firewall']['trusted_proxy_header'];
573 if ( isset( self::trusted_header_map()[ $header ] ) ) {
574 return $header;
575 }
576 }
577 return '';
578 }
579
580 /**
581 * The proxy IPs/CIDRs the admin declared their forwarded header comes from.
582 *
583 * @since 2.11.9
584 *
585 * @return string[]
586 */
587 public static function trusted_proxies() {
588 $options = get_option( 'vigilante_options' );
589 $list = ( is_array( $options ) && isset( $options['firewall']['trusted_proxies'] ) ) ? $options['firewall']['trusted_proxies'] : array();
590 return is_array( $list ) ? $list : array();
591 }
592
593 /**
594 * Cloudflare's published edge ranges, so CF-Connecting-IP verifies itself.
595 *
596 * From https://www.cloudflare.com/ips/ (stable, changes rarely). Bundled so
597 * a site behind Cloudflare does not have to list them by hand; if they ever
598 * change, the admin can add the new ones to the trusted proxies list.
599 *
600 * @since 2.11.9
601 *
602 * @return string[]
603 */
604 public static function cloudflare_ranges() {
605 return array(
606 '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
607 '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
608 '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
609 '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
610 '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32',
611 '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32',
612 );
613 }
614
615 /**
616 * Whether the TCP peer may be trusted to have set the forwarded header
617 *
618 * The reviewer of 2.11.8 was right: honouring CF-Connecting-IP,
619 * X-Forwarded-For or X-Real-IP without checking who sent them lets any
620 * visitor whose request reaches PHP directly forge the address the firewall,
621 * the whitelist and the rate limiter act on. So the header is honoured only
622 * when the real connection, REMOTE_ADDR, is a proxy we have reason to trust:
623 *
624 * - an exact address or CIDR range in the admin's trusted proxies list
625 * (wins for any header); a wildcard there is ignored, see
626 * in_list_ip_or_cidr();
627 * - for CF-Connecting-IP, one of Cloudflare's published ranges, since only
628 * Cloudflare sends that header;
629 * - with no list configured, an address of your own network (a reverse proxy
630 * in front of PHP, a load balancer in a private subnet), which a visitor
631 * hitting a public origin directly is not.
632 *
633 * A public load balancer that connects from a public address needs its IPs
634 * in the trusted proxies list; until then its header is not honoured and the
635 * connection address is used, which is safe.
636 *
637 * @since 2.11.9
638 *
639 * @param string $remote Validated REMOTE_ADDR.
640 * @param string $header Trusted header key.
641 * @param string[] $trusted_proxies Configured proxy IPs/CIDRs.
642 * @return bool
643 */
644 private static function peer_is_trusted_proxy( $remote, $header, $trusted_proxies ) {
645 // A dual-stack proxy connects as ::ffff:10.0.0.5; read it as the IPv4 it
646 // is, so a private reverse proxy is recognised as own network and a peer
647 // listed by its IPv4 matches. client_from_chain() already unmaps, this
648 // keeps the two sides symmetric (found by the cross review of 2.11.9).
649 $remote = self::unmap_ipv4( $remote );
650
651 if ( ! empty( $trusted_proxies ) && self::in_list_ip_or_cidr( $remote, $trusted_proxies ) ) {
652 return true;
653 }
654
655 if ( 'cf-connecting-ip' === $header && self::in_list_ip_or_cidr( $remote, self::cloudflare_ranges() ) ) {
656 return true;
657 }
658
659 return empty( $trusted_proxies ) && self::is_own_network( $remote );
660 }
661
662 /**
663 * Resolve the client IP from a $_SERVER-like array.
664 *
665 * Only the real TCP peer (REMOTE_ADDR) is trusted by default, because it
666 * cannot be spoofed. A forwarded-for / connecting-ip header is honoured only
667 * when the admin has declared their site sits behind that proxy AND the
668 * connection actually comes from a proxy we trust (see
669 * peer_is_trusted_proxy()); otherwise any visitor could forge the header and
670 * impersonate any IP, bypassing the whitelist, evading the blacklist and
671 * poisoning the rate limiter. Reported by the wp.org review of 2.11.8.
672 *
673 * @param array $server A $_SERVER-like array.
674 * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none.
675 * @param string[] $trusted_proxies Configured proxy IPs/CIDRs.
676 * @return string Validated IP, or '0.0.0.0' when none could be determined.
677 */
678 public static function resolve_client_ip( $server, $trusted_header = '', $trusted_proxies = array() ) {
679 $map = self::trusted_header_map();
680 $remote = '';
681
682 if ( ! empty( $server['REMOTE_ADDR'] ) ) {
683 $candidate = trim( (string) $server['REMOTE_ADDR'] );
684 if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
685 $remote = $candidate;
686 }
687 }
688
689 if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) && '' !== $remote
690 && ! empty( $server[ $map[ $trusted_header ] ] )
691 && self::peer_is_trusted_proxy( $remote, $trusted_header, $trusted_proxies )
692 ) {
693 $value = self::client_from_chain( (string) $server[ $map[ $trusted_header ] ] );
694 if ( '' !== $value ) {
695 return $value;
696 }
697 }
698
699 return '' !== $remote ? $remote : '0.0.0.0';
700 }
701
702 /**
703 * The visitor address in a forwarded header, read from the proxy's end
704 *
705 * A proxy adds the address it received the connection from to the END of
706 * X-Forwarded-For, and keeps whatever the visitor sent in front of it. So
707 * in "a, b, c" the visitor wrote a and b, and only c was written by the
708 * proxy the site trusts. Until 2.11.7 this took the first entry, the one
709 * the visitor chooses, and on a site set to X-Forwarded-For anybody could
710 * pick the address the firewall saw: out of the blacklist, into the
711 * whitelist, a new address per request for the rate limit and the login
712 * lockout. Found by the audit of the firewall for 2.11.8.
713 *
714 * Read from the right, an address of the site's own network (see
715 * is_own_network()) is taken as one more proxy and passed over, and the
716 * first address outside it is the visitor. When there is none, the nearest
717 * valid address is. An entry that is not an address stops the reading,
718 * since nothing left of it can be told apart from what the visitor wrote,
719 * and the caller falls back to the connection address.
720 *
721 * The first version of this, in the same release, told the two apart with
722 * FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, and what those
723 * flags cover changes with the PHP version: from 8.3 an IPv4 address
724 * written as IPv6 (::ffff:a.b.c.d, as a dual stack proxy writes it) counts
725 * as reserved, so it was passed over and the visitor's own entry won
726 * again. Found by the cross review of 2.11.8. The ranges are written out
727 * now, and a mapped address is read as the IPv4 it is.
728 *
729 * Behind a CDN with a reverse proxy in front of PHP that adds to the
730 * header, or a load balancer that adds its own public address, this reads
731 * the address of that CDN or balancer. That is the price of not believing
732 * the visitor; the header of the CDN itself is the setting that fits
733 * there, and the Firewall tab says so when the administrator's own request
734 * shows that shape.
735 *
736 * @since 2.11.8
737 *
738 * @param string $value Header value.
739 * @return string Address, or '' when there is none to trust.
740 */
741 public static function client_from_chain( $value ) {
742 $entries = array_reverse( array_map( 'trim', explode( ',', (string) $value ) ) );
743 $nearest = '';
744
745 foreach ( $entries as $entry ) {
746 $address = self::unmap_ipv4( $entry );
747
748 if ( ! filter_var( $address, FILTER_VALIDATE_IP ) ) {
749 break;
750 }
751
752 if ( ! self::is_own_network( $address ) ) {
753 return $address;
754 }
755
756 if ( '' === $nearest ) {
757 $nearest = $address;
758 }
759 }
760
761 return $nearest;
762 }
763
764 /**
765 * The X-Forwarded-For header of this request, when the site trusts it
766 *
767 * Only for showing: the Firewall tab compares both readings of the
768 * administrator's own request. The firewall resolves the address with
769 * get_client_ip(). It lives here so that every read of a proxy header stays
770 * in this class, which a permanent harness checks.
771 *
772 * @since 2.11.8
773 *
774 * @return string Header value, or '' when it is not trusted or not sent.
775 */
776 public static function trusted_forwarded_for() {
777 if ( 'x-forwarded-for' !== self::trusted_proxy_header() || ! isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
778 return '';
779 }
780
781 return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
782 }
783
784 /**
785 * Whether an address belongs to a network no visitor comes from
786 *
787 * Private, loopback, link-local and the shared address space providers use
788 * inside their own networks, for IPv4 and IPv6. Written out rather than
789 * taken from filter_var() flags, whose ranges change between PHP versions.
790 *
791 * @since 2.11.8
792 *
793 * @param string $address Valid IP address, IPv4 written as IPv4.
794 * @return bool
795 */
796 public static function is_own_network( $address ) {
797 $ranges = array(
798 '10.0.0.0/8',
799 '172.16.0.0/12',
800 '192.168.0.0/16',
801 '127.0.0.0/8',
802 '169.254.0.0/16',
803 '100.64.0.0/10',
804 '::1/128',
805 'fc00::/7',
806 'fe80::/10',
807 );
808
809 foreach ( $ranges as $range ) {
810 if ( self::cidr_match( $address, $range ) ) {
811 return true;
812 }
813 }
814
815 return false;
816 }
817
818 /**
819 * An IPv4 address written as IPv6, as the IPv4 address it is
820 *
821 * Covers both spellings, ::ffff:203.0.113.7 and ::ffff:cb00:7107. Anything
822 * else comes back as it was.
823 *
824 * @since 2.11.8
825 *
826 * @param string $address Address as written in the header.
827 * @return string
828 */
829 public static function unmap_ipv4( $address ) {
830 if ( ! filter_var( $address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
831 return $address;
832 }
833
834 $packed = inet_pton( $address );
835
836 if ( false !== $packed && 16 === strlen( $packed ) && str_repeat( "\0", 10 ) . "\xff\xff" === substr( $packed, 0, 12 ) ) {
837 $ipv4 = inet_ntop( substr( $packed, 12 ) );
838
839 return false === $ipv4 ? $address : $ipv4;
840 }
841
842 return $address;
843 }
844
845 /**
846 * Current request client IP, honouring the configured trusted proxy header.
847 *
848 * Reads only the needed headers, each sanitized at the point of access, so
849 * the input-sanitization sniff is satisfied without any suppression.
850 *
851 * @return string
852 */
853 public static function get_client_ip() {
854 $trusted = self::trusted_proxy_header();
855 $server = array();
856
857 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
858 $server['REMOTE_ADDR'] = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
859 }
860
861 if ( '' !== $trusted ) {
862 $map = self::trusted_header_map();
863 $key = $map[ $trusted ];
864 if ( isset( $_SERVER[ $key ] ) ) {
865 $server[ $key ] = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
866 }
867 }
868
869 return self::resolve_client_ip( $server, $trusted, self::trusted_proxies() );
870 }
871 }
872