| @@ -375,17 +375,10 @@ | ||
| 375 | 375 | |
| 376 | 376 | if ( '' !== $trusted_header && isset( $map[ $trusted_header ] ) ) { |
| 377 | 377 | $key = $map[ $trusted_header ]; |
| 378 | 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 ) ) { | |
| 379 | + $value = self::client_from_chain( (string) $server[ $key ] ); | |
| 380 | + if ( '' !== $value ) { | |
| 388 | 381 | return $value; |
| 389 | 382 | } |
| 390 | 383 | } |
| 391 | 384 | } |
| @@ -397,8 +390,151 @@ | ||
| 397 | 390 | } |
| 398 | 391 | } |
| 399 | 392 | |
| 400 | 393 | return '0.0.0.0'; |
| 394 | + } | |
| 395 | + | |
| 396 | + /** | |
| 397 | + * The visitor address in a forwarded header, read from the proxy's end | |
| 398 | + * | |
| 399 | + * A proxy adds the address it received the connection from to the END of | |
| 400 | + * X-Forwarded-For, and keeps whatever the visitor sent in front of it. So | |
| 401 | + * in "a, b, c" the visitor wrote a and b, and only c was written by the | |
| 402 | + * proxy the site trusts. Until 2.11.7 this took the first entry, the one | |
| 403 | + * the visitor chooses, and on a site set to X-Forwarded-For anybody could | |
| 404 | + * pick the address the firewall saw: out of the blacklist, into the | |
| 405 | + * whitelist, a new address per request for the rate limit and the login | |
| 406 | + * lockout. Found by the audit of the firewall for 2.11.8. | |
| 407 | + * | |
| 408 | + * Read from the right, an address of the site's own network (see | |
| 409 | + * is_own_network()) is taken as one more proxy and passed over, and the | |
| 410 | + * first address outside it is the visitor. When there is none, the nearest | |
| 411 | + * valid address is. An entry that is not an address stops the reading, | |
| 412 | + * since nothing left of it can be told apart from what the visitor wrote, | |
| 413 | + * and the caller falls back to the connection address. | |
| 414 | + * | |
| 415 | + * The first version of this, in the same release, told the two apart with | |
| 416 | + * FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, and what those | |
| 417 | + * flags cover changes with the PHP version: from 8.3 an IPv4 address | |
| 418 | + * written as IPv6 (::ffff:a.b.c.d, as a dual stack proxy writes it) counts | |
| 419 | + * as reserved, so it was passed over and the visitor's own entry won | |
| 420 | + * again. Found by the cross review of 2.11.8. The ranges are written out | |
| 421 | + * now, and a mapped address is read as the IPv4 it is. | |
| 422 | + * | |
| 423 | + * Behind a CDN with a reverse proxy in front of PHP that adds to the | |
| 424 | + * header, or a load balancer that adds its own public address, this reads | |
| 425 | + * the address of that CDN or balancer. That is the price of not believing | |
| 426 | + * the visitor; the header of the CDN itself is the setting that fits | |
| 427 | + * there, and the Firewall tab says so when the administrator's own request | |
| 428 | + * shows that shape. | |
| 429 | + * | |
| 430 | + * @since 2.11.8 | |
| 431 | + * | |
| 432 | + * @param string $value Header value. | |
| 433 | + * @return string Address, or '' when there is none to trust. | |
| 434 | + */ | |
| 435 | + public static function client_from_chain( $value ) { | |
| 436 | + $entries = array_reverse( array_map( 'trim', explode( ',', (string) $value ) ) ); | |
| 437 | + $nearest = ''; | |
| 438 | + | |
| 439 | + foreach ( $entries as $entry ) { | |
| 440 | + $address = self::unmap_ipv4( $entry ); | |
| 441 | + | |
| 442 | + if ( ! filter_var( $address, FILTER_VALIDATE_IP ) ) { | |
| 443 | + break; | |
| 444 | + } | |
| 445 | + | |
| 446 | + if ( ! self::is_own_network( $address ) ) { | |
| 447 | + return $address; | |
| 448 | + } | |
| 449 | + | |
| 450 | + if ( '' === $nearest ) { | |
| 451 | + $nearest = $address; | |
| 452 | + } | |
| 453 | + } | |
| 454 | + | |
| 455 | + return $nearest; | |
| 456 | + } | |
| 457 | + | |
| 458 | + /** | |
| 459 | + * The X-Forwarded-For header of this request, when the site trusts it | |
| 460 | + * | |
| 461 | + * Only for showing: the Firewall tab compares both readings of the | |
| 462 | + * administrator's own request. The firewall resolves the address with | |
| 463 | + * get_client_ip(). It lives here so that every read of a proxy header stays | |
| 464 | + * in this class, which a permanent harness checks. | |
| 465 | + * | |
| 466 | + * @since 2.11.8 | |
| 467 | + * | |
| 468 | + * @return string Header value, or '' when it is not trusted or not sent. | |
| 469 | + */ | |
| 470 | + public static function trusted_forwarded_for() { | |
| 471 | + if ( 'x-forwarded-for' !== self::trusted_proxy_header() || ! isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { | |
| 472 | + return ''; | |
| 473 | + } | |
| 474 | + | |
| 475 | + return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); | |
| 476 | + } | |
| 477 | + | |
| 478 | + /** | |
| 479 | + * Whether an address belongs to a network no visitor comes from | |
| 480 | + * | |
| 481 | + * Private, loopback, link-local and the shared address space providers use | |
| 482 | + * inside their own networks, for IPv4 and IPv6. Written out rather than | |
| 483 | + * taken from filter_var() flags, whose ranges change between PHP versions. | |
| 484 | + * | |
| 485 | + * @since 2.11.8 | |
| 486 | + * | |
| 487 | + * @param string $address Valid IP address, IPv4 written as IPv4. | |
| 488 | + * @return bool | |
| 489 | + */ | |
| 490 | + public static function is_own_network( $address ) { | |
| 491 | + $ranges = array( | |
| 492 | + '10.0.0.0/8', | |
| 493 | + '172.16.0.0/12', | |
| 494 | + '192.168.0.0/16', | |
| 495 | + '127.0.0.0/8', | |
| 496 | + '169.254.0.0/16', | |
| 497 | + '100.64.0.0/10', | |
| 498 | + '::1/128', | |
| 499 | + 'fc00::/7', | |
| 500 | + 'fe80::/10', | |
| 501 | + ); | |
| 502 | + | |
| 503 | + foreach ( $ranges as $range ) { | |
| 504 | + if ( self::cidr_match( $address, $range ) ) { | |
| 505 | + return true; | |
| 506 | + } | |
| 507 | + } | |
| 508 | + | |
| 509 | + return false; | |
| 510 | + } | |
| 511 | + | |
| 512 | + /** | |
| 513 | + * An IPv4 address written as IPv6, as the IPv4 address it is | |
| 514 | + * | |
| 515 | + * Covers both spellings, ::ffff:203.0.113.7 and ::ffff:cb00:7107. Anything | |
| 516 | + * else comes back as it was. | |
| 517 | + * | |
| 518 | + * @since 2.11.8 | |
| 519 | + * | |
| 520 | + * @param string $address Address as written in the header. | |
| 521 | + * @return string | |
| 522 | + */ | |
| 523 | + public static function unmap_ipv4( $address ) { | |
| 524 | + if ( ! filter_var( $address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { | |
| 525 | + return $address; | |
| 526 | + } | |
| 527 | + | |
| 528 | + $packed = inet_pton( $address ); | |
| 529 | + | |
| 530 | + if ( false !== $packed && 16 === strlen( $packed ) && str_repeat( "\0", 10 ) . "\xff\xff" === substr( $packed, 0, 12 ) ) { | |
| 531 | + $ipv4 = inet_ntop( substr( $packed, 12 ) ); | |
| 532 | + | |
| 533 | + return false === $ipv4 ? $address : $ipv4; | |
| 534 | + } | |
| 535 | + | |
| 536 | + return $address; | |
| 401 | 537 | } |
| 402 | 538 | |
| 403 | 539 | /** |
| 404 | 540 | * Current request client IP, honouring the configured trusted proxy header. |