| @@ -39,10 +39,11 @@ | ||
| 39 | 39 | if ( '' === $ip || '' === $pattern ) { |
| 40 | 40 | return false; |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | - // Exact match (also covers fully-written IPv6). | |
| 44 | - if ( $ip === $pattern ) { | |
| 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 ) ) { | |
| 45 | 46 | return true; |
| 46 | 47 | } |
| 47 | 48 | |
| 48 | 49 | // CIDR notation. |
| @@ -79,8 +80,65 @@ | ||
| 79 | 80 | return false; |
| 80 | 81 | } |
| 81 | 82 | |
| 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 | + /** | |
| 83 | 141 | * Whether a string is a pattern this class can actually match. |
| 84 | 142 | * |
| 85 | 143 | * The counterpart of matches(): everything this returns true for is |
| 86 | 144 | * something the matcher understands, and everything else is noise that |
| @@ -176,8 +234,171 @@ | ||
| 176 | 234 | ); |
| 177 | 235 | } |
| 178 | 236 | |
| 179 | 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 | + /** | |
| 180 | 401 | * Whether a wildcard pattern is plausible for one address family. |
| 181 | 402 | * |
| 182 | 403 | * A bare '*' is rejected on purpose: as a whitelist it would let everyone |
| 183 | 404 | * in and as a blacklist it would lock everyone out, and nobody types that |
| @@ -356,52 +577,273 @@ | ||
| 356 | 577 | return ''; |
| 357 | 578 | } |
| 358 | 579 | |
| 359 | 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 | + /** | |
| 360 | 663 | * Resolve the client IP from a $_SERVER-like array. |
| 361 | 664 | * |
| 362 | 665 | * Only the real TCP peer (REMOTE_ADDR) is trusted by default, because it |
| 363 | - * cannot be spoofed. A forwarded-for / connecting-ip header is honoured | |
| 364 | - * ONLY when the admin has explicitly declared their site sits behind that | |
| 365 | - * proxy; otherwise any visitor could forge the header and impersonate any | |
| 366 | - * IP (bypassing the whitelist, evading the blacklist, poisoning the rate | |
| 367 | - * limiter, etc.). | |
| 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. | |
| 368 | 672 | * |
| 369 | - * @param array $server A $_SERVER-like array. | |
| 370 | - * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none. | |
| 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. | |
| 371 | 676 | * @return string Validated IP, or '0.0.0.0' when none could be determined. |
| 372 | 677 | */ |
| 373 | - public static function resolve_client_ip( $server, $trusted_header = '' ) { | |
| 374 | - $map = self::trusted_header_map(); | |
| 678 | + public static function resolve_client_ip( $server, $trusted_header = '', $trusted_proxies = array() ) { | |
| 679 | + $map = self::trusted_header_map(); | |
| 680 | + $remote = ''; | |
| 375 | 681 | |
| 376 | - if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) ) { | |
| 377 | - $key = $map[ $trusted_header ]; | |
| 378 | - if ( ! empty( $server[ $key ] ) ) { | |
| 379 | - $value = (string) $server[ $key ]; | |
| 380 | - // X-Forwarded-For may be a "client, proxy1, proxy2" chain; the | |
| 381 | - // original client is the first entry. | |
| 382 | - if ( false !== strpos( $value, ',' ) ) { | |
| 383 | - $parts = explode( ',', $value ); | |
| 384 | - $value = $parts[0]; | |
| 385 | - } | |
| 386 | - $value = trim( $value ); | |
| 387 | - if ( filter_var( $value, FILTER_VALIDATE_IP ) ) { | |
| 388 | - return $value; | |
| 389 | - } | |
| 682 | + if ( ! empty( $server['REMOTE_ADDR'] ) ) { | |
| 683 | + $candidate = trim( (string) $server['REMOTE_ADDR'] ); | |
| 684 | + if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) { | |
| 685 | + $remote = $candidate; | |
| 390 | 686 | } |
| 391 | 687 | } |
| 392 | 688 | |
| 393 | - if ( ! empty( $server['REMOTE_ADDR'] ) ) { | |
| 394 | - $remote = trim( (string) $server['REMOTE_ADDR'] ); | |
| 395 | - if ( filter_var( $remote, FILTER_VALIDATE_IP ) ) { | |
| 396 | - return $remote; | |
| 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; | |
| 397 | 696 | } |
| 398 | 697 | } |
| 399 | 698 | |
| 400 | - return '0.0.0.0'; | |
| 699 | + return '' !== $remote ? $remote : '0.0.0.0'; | |
| 401 | 700 | } |
| 402 | 701 | |
| 403 | 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 | + /** | |
| 404 | 846 | * Current request client IP, honouring the configured trusted proxy header. |
| 405 | 847 | * |
| 406 | 848 | * Reads only the needed headers, each sanitized at the point of access, so |
| 407 | 849 | * the input-sanitization sniff is satisfied without any suppression. |
| @@ -423,7 +865,7 @@ | ||
| 423 | 865 | $server[ $key ] = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); |
| 424 | 866 | } |
| 425 | 867 | } |
| 426 | 868 | |
| 427 | - return self::resolve_client_ip( $server, $trusted ); | |
| 869 | + return self::resolve_client_ip( $server, $trusted, self::trusted_proxies() ); | |
| 428 | 870 | } |
| 429 | 871 | } |