PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 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 All 88 releases
← All changes | includes/class-ip-utils.php +145 -9 2.11.32.11.8 View file →
@@ -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.