diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index b343bb69f572b..9b8f3bd648ffd 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -589,7 +589,48 @@ function wp_http_validate_url( $url ) { $host = trim( $parsed_url['host'], '.' ); if ( ! $same_host ) { - if ( preg_match( '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', $host ) ) { + $is_ipv4 = (bool) preg_match( + '#^(([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)\.){3}([1-9]?\d|1\d\d|25[0-5]|2[0-4]\d)$#', + $host + ); + + $is_ipv6_literal = ( 0 === strpos( $host, '[' ) ); + + if ( extension_loaded( 'filter' ) && ! $is_ipv4 && ! $is_ipv6_literal ) { + $host_to_validate = $host; + + /* + * Historically wp_http_validate_url() has allowed underscores in subdomains + * (e.g. some legacy Blogspot hosts). If underscores are present, validate + * only the registrable domain portion (last two labels) using FILTER_FLAG_HOSTNAME. + */ + if ( false !== strpos( $host, '_' ) ) { + $labels = explode( '.', $host ); + + if ( count( $labels ) < 2 ) { + return false; + } + + $host_to_validate = implode( '.', array_slice( $labels, -2 ) ); + + // Underscores must not appear in the registrable domain portion. + if ( false !== strpos( $host_to_validate, '_' ) ) { + return false; + } + } + + if ( + false === filter_var( + $host_to_validate, + FILTER_VALIDATE_DOMAIN, + array( 'flags' => FILTER_FLAG_HOSTNAME ) + ) + ) { + return false; + } + } + + if ( $is_ipv4 ) { $ip = $host; } else { $ip = gethostbyname( $host ); diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 651064dc5674c..3c61cc4aba4ff 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -566,6 +566,9 @@ public function data_wp_http_validate_url_should_not_validate() { 'url' => 'https://example.com:81/caniload.php', 'cb_safe_ports' => 'callback_remove_safe_ports', ), + 'underscore_in_hostname' => array( + 'url' => 'https://foo_bar.example.com/', + ), ); } @@ -713,4 +716,40 @@ public function test_normalize_cookies_casts_cookie_name_integer_to_string() { $this->assertInstanceOf( 'WpOrg\Requests\Cookie\Jar', $cookie_jar ); $this->assertInstanceOf( 'WpOrg\Requests\Cookie', $cookie_jar['1'] ); } + + /** + * @ticket 64457 + * + * Ensure hostname validation does not regress valid edge cases + * while rejecting clearly invalid hosts. + */ + public function test_wp_http_validate_url_hostname_edge_cases() { + $this->assertFalse( + wp_http_validate_url( 'h_ttp://example.org' ), + 'Underscore in scheme should be invalid.' + ); + + $this->assertFalse( + wp_http_validate_url( 'https://hey_ho_lets_go._example.org' ), + 'Underscore in a standalone label should be invalid.' + ); + + $this->assertFalse( + wp_http_validate_url( 'https://omg.c_om' ), + 'Underscore in TLD should be invalid.' + ); + + $old_home = get_option( 'home' ); + + try { + update_option( 'home', 'https://peter_is_amazing.example.org' ); + + $this->assertNotFalse( + wp_http_validate_url( 'https://peter_is_amazing.example.org' ), + 'Underscores in subdomain should remain valid.' + ); + } finally { + update_option( 'home', $old_home ); + } + } }