diff --git a/src/wp-admin/includes/class-wp-community-events.php b/src/wp-admin/includes/class-wp-community-events.php index a204f9cafe..79c4a85adb 100644 --- a/src/wp-admin/includes/class-wp-community-events.php +++ b/src/wp-admin/includes/class-wp-community-events.php @@ -233,7 +233,8 @@ class WP_Community_Events { * or false on failure. */ public static function get_unsafe_client_ip() { - $client_ip = false; + $client_ip = $netmask = false; + $ip_prefix = ''; // In order of preference, with the best ones for this purpose first. $address_headers = array( @@ -260,18 +261,47 @@ class WP_Community_Events { } } - // These functions are not available on Windows until PHP 5.3. - if ( function_exists( 'inet_pton' ) && function_exists( 'inet_ntop' ) ) { - if ( 4 === strlen( inet_pton( $client_ip ) ) ) { - $netmask = '255.255.255.0'; // ipv4. - } else { - $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; // ipv6. - } - - $client_ip = inet_ntop( inet_pton( $client_ip ) & inet_pton( $netmask ) ); + if ( ! $client_ip ) { + return false; } - return $client_ip; + // Detect what kind of IP address this is. + $is_ipv6 = substr_count( $client_ip, ':' ) > 1; + $is_ipv4 = ( 3 === substr_count( $client_ip, '.' ) ); + + if ( $is_ipv6 && $is_ipv4 ) { + // IPv6 compatibility mode, temporarily strip the IPv6 part, and treat it like IPv4. + $ip_prefix = '::ffff:'; + $client_ip = preg_replace( '/^\[?[0-9a-f:]*:/i', '', $client_ip ); + $client_ip = str_replace( ']', '', $client_ip ); + $is_ipv6 = false; + } + + if ( $is_ipv6 ) { + // IPv6 addresses will always be enclosed in [] if there's a port. + $ip_start = 1; + $ip_end = (int) strpos( $client_ip, ']' ) - 1; + $netmask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + + // Strip the port (and [] from IPv6 addresses), if they exist. + if ( $ip_end > 0 ) { + $client_ip = substr( $client_ip, $ip_start, $ip_end ); + } + + // Partially anonymize the IP by reducing it to the corresponding network ID. + if ( function_exists( 'inet_pton' ) && function_exists( 'inet_ntop' ) ) { + $client_ip = inet_ntop( inet_pton( $client_ip ) & inet_pton( $netmask ) ); + } + } elseif ( $is_ipv4 ) { + // Strip any port and partially anonymize the IP. + $last_octet_position = strrpos( $client_ip, '.' ); + $client_ip = substr( $client_ip, 0, $last_octet_position ) . '.0'; + } else { + return false; + } + + // Restore the IPv6 prefix to compatibility mode addresses. + return $ip_prefix . $client_ip; } /** diff --git a/tests/phpunit/tests/admin/includesCommunityEvents.php b/tests/phpunit/tests/admin/includesCommunityEvents.php index ed5cc2caee..50b5623a28 100644 --- a/tests/phpunit/tests/admin/includesCommunityEvents.php +++ b/tests/phpunit/tests/admin/includesCommunityEvents.php @@ -255,4 +255,113 @@ class Test_WP_Community_Events extends WP_UnitTestCase { 'filename' => '', ); } + + /** + * Test that get_unsafe_client_ip() properly anonymizes all possible address formats + * + * @dataProvider data_get_unsafe_client_ip_anonymization + * + * @ticket 41083 + */ + public function test_get_unsafe_client_ip_anonymization( $raw_ip, $expected_result ) { + $_SERVER['REMOTE_ADDR'] = $raw_ip; + $actual_result = WP_Community_Events::get_unsafe_client_ip(); + + $this->assertEquals( $expected_result, $actual_result ); + } + + public function data_get_unsafe_client_ip_anonymization() { + return array( + // Invalid IP. + array( + '', // Raw IP address + false, // Expected result + ), + // Invalid IP. Sometimes proxies add things like this, or other arbitrary strings. + array( + 'unknown', + false, + ), + // IPv4, no port + array( + '10.20.30.45', + '10.20.30.0', + ), + // IPv4, port + array( + '10.20.30.45:20000', + '10.20.30.0', + ), + // IPv6, no port + array( + '2a03:2880:2110:df07:face:b00c::1', + '2a03:2880:2110:df07::', + ), + // IPv6, port + array( + '[2a03:2880:2110:df07:face:b00c::1]:20000', + '2a03:2880:2110:df07::', + ), + // IPv6, no port, reducible representation + array( + '0000:0000:0000:0000:0000:0000:0000:0001', + '::', + ), + // IPv6, no port, partially reducible representation + array( + '1000:0000:0000:0000:0000:0000:0000:0001', + '1000::', + ), + // IPv6, port, reducible representation + array( + '[0000:0000:0000:0000:0000:0000:0000:0001]:1234', + '::', + ), + // IPv6, port, partially reducible representation + array( + '[1000:0000:0000:0000:0000:0000:0000:0001]:5678', + '1000::', + ), + // IPv6, no port, reduced representation + array( + '::', + '::', + ), + // IPv6, no port, reduced representation + array( + '::1', + '::', + ), + // IPv6, port, reduced representation + array( + '[::]:20000', + '::', + ), + // IPv6, address brackets without port delimiter and number, reduced representation + array( + '[::1]', + '::', + ), + // IPv6, no port, compatibility mode + array( + '::ffff:10.15.20.25', + '::ffff:10.15.20.0', + ), + // IPv6, port, compatibility mode + array( + '[::ffff:10.15.20.25]:30000', + '::ffff:10.15.20.0', + ), + // IPv6, no port, compatibility mode shorthand + array( + '::127.0.0.1', + '::ffff:127.0.0.0', + ), + // IPv6, port, compatibility mode shorthand + array( + '[::127.0.0.1]:30000', + '::ffff:127.0.0.0', + ), + ); + } }