| @@ -39,11 +39,10 @@ | ||
| 39 | 39 | if ( '' === $ip || '' === $pattern ) { |
| 40 | 40 | return false; |
| 41 | 41 | } |
| 42 | 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 ) ) { | |
| 43 | + // Exact match (also covers fully-written IPv6). | |
| 44 | + if ( $ip === $pattern ) { | |
| 46 | 45 | return true; |
| 47 | 46 | } |
| 48 | 47 | |
| 49 | 48 | // CIDR notation. |
| @@ -80,65 +79,8 @@ | ||
| 80 | 79 | return false; |
| 81 | 80 | } |
| 82 | 81 | |
| 83 | 82 | /** |
| 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 | 83 | * Whether a string is a pattern this class can actually match. |
| 142 | 84 | * |
| 143 | 85 | * The counterpart of matches(): everything this returns true for is |
| 144 | 86 | * something the matcher understands, and everything else is noise that |
| @@ -234,171 +176,8 @@ | ||
| 234 | 176 | ); |
| 235 | 177 | } |
| 236 | 178 | |
| 237 | 179 | /** |
| 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 | 180 | * Whether a wildcard pattern is plausible for one address family. |
| 402 | 181 | * |
| 403 | 182 | * A bare '*' is rejected on purpose: as a whitelist it would let everyone |
| 404 | 183 | * in and as a blacklist it would lock everyone out, and nobody types that |
| @@ -577,127 +356,42 @@ | ||
| 577 | 356 | return ''; |
| 578 | 357 | } |
| 579 | 358 | |
| 580 | 359 | /** |
| 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 | 360 | * Resolve the client IP from a $_SERVER-like array. |
| 664 | 361 | * |
| 665 | 362 | * 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. | |
| 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.). | |
| 672 | 368 | * |
| 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. | |
| 369 | + * @param array $server A $_SERVER-like array. | |
| 370 | + * @param string $trusted_header One of the keys in trusted_header_map(), or '' for none. | |
| 676 | 371 | * @return string Validated IP, or '0.0.0.0' when none could be determined. |
| 677 | 372 | */ |
| 678 | - public static function resolve_client_ip( $server, $trusted_header = '', $trusted_proxies = array() ) { | |
| 679 | - $map = self::trusted_header_map(); | |
| 680 | - $remote = ''; | |
| 373 | + public static function resolve_client_ip( $server, $trusted_header = '' ) { | |
| 374 | + $map = self::trusted_header_map(); | |
| 681 | 375 | |
| 682 | - if ( ! empty( $server['REMOTE_ADDR'] ) ) { | |
| 683 | - $candidate = trim( (string) $server['REMOTE_ADDR'] ); | |
| 684 | - if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) { | |
| 685 | - $remote = $candidate; | |
| 376 | + if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) ) { | |
| 377 | + $key = $map[ $trusted_header ]; | |
| 378 | + if ( ! empty( $server[ $key ] ) ) { | |
| 379 | + $value = self::client_from_chain( (string) $server[ $key ] ); | |
| 380 | + if ( '' !== $value ) { | |
| 381 | + return $value; | |
| 382 | + } | |
| 686 | 383 | } |
| 687 | 384 | } |
| 688 | 385 | |
| 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; | |
| 386 | + if ( ! empty( $server['REMOTE_ADDR'] ) ) { | |
| 387 | + $remote = trim( (string) $server['REMOTE_ADDR'] ); | |
| 388 | + if ( filter_var( $remote, FILTER_VALIDATE_IP ) ) { | |
| 389 | + return $remote; | |
| 696 | 390 | } |
| 697 | 391 | } |
| 698 | 392 | |
| 699 | - return '' !== $remote ? $remote : '0.0.0.0'; | |
| 393 | + return '0.0.0.0'; | |
| 700 | 394 | } |
| 701 | 395 | |
| 702 | 396 | /** |
| 703 | 397 | * The visitor address in a forwarded header, read from the proxy's end |
| @@ -865,7 +559,7 @@ | ||
| 865 | 559 | $server[ $key ] = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); |
| 866 | 560 | } |
| 867 | 561 | } |
| 868 | 562 | |
| 869 | - return self::resolve_client_ip( $server, $trusted, self::trusted_proxies() ); | |
| 563 | + return self::resolve_client_ip( $server, $trusted ); | |
| 870 | 564 | } |
| 871 | 565 | } |