From b89bec6500bd2a31f72069b98b91c7594e033c8a Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 10:57:01 +0700 Subject: [PATCH 01/11] HTTP API: Early filter invalid hosts in wp_http_validate_url() --- src/wp-includes/http.php | 9 +++++++++ tests/phpunit/tests/http/http.php | 3 +++ 2 files changed, 12 insertions(+) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index b343bb69f572b..c13d0d396752c 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -589,6 +589,15 @@ function wp_http_validate_url( $url ) { $host = trim( $parsed_url['host'], '.' ); if ( ! $same_host ) { + if ( + function_exists( 'filter_var' ) + && defined( 'FILTER_VALIDATE_DOMAIN' ) + && defined( 'FILTER_FLAG_HOSTNAME' ) + && ! filter_var( $host, FILTER_VALIDATE_IP ) + && ! filter_var( $host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ) + ) { + return false; + } 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 ) ) { $ip = $host; } else { diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 651064dc5674c..86b4fbb81a2ad 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/', + ), ); } From 1439d456c33f1ee5134a08acd31f912b58af194f Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 11:58:10 +0700 Subject: [PATCH 02/11] Validate non-IP hostnames early in wp_http_validate_url --- src/wp-includes/http.php | 19 +++++++++++++------ tests/phpunit/tests/http/http.php | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index c13d0d396752c..3d94521eaeabf 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -589,16 +589,23 @@ function wp_http_validate_url( $url ) { $host = trim( $parsed_url['host'], '.' ); if ( ! $same_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 + ); + if ( - function_exists( 'filter_var' ) - && defined( 'FILTER_VALIDATE_DOMAIN' ) - && defined( 'FILTER_FLAG_HOSTNAME' ) - && ! filter_var( $host, FILTER_VALIDATE_IP ) - && ! filter_var( $host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ) + ! $is_ipv4 + && function_exists( 'filter_var' ) + && ! filter_var( + $host, + FILTER_VALIDATE_DOMAIN, + array( 'flags' => FILTER_FLAG_HOSTNAME ) + ) ) { return false; } - 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 ) ) { + 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 86b4fbb81a2ad..e3f18d4d27f89 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -566,8 +566,8 @@ 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/', + 'underscore_in_hostname' => array( + 'url' => 'https://foo_bar.example.com/', ), ); } From fc38d7ab7e1f25d6af1ff59696dcf39cbd3b9b62 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 14:36:27 +0700 Subject: [PATCH 03/11] HTTP: Early reject invalid hostnames in wp_http_validate_url() --- src/wp-includes/http.php | 2 +- tests/phpunit/tests/http/http.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index 3d94521eaeabf..c1bf29f41ddbf 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -596,7 +596,7 @@ function wp_http_validate_url( $url ) { if ( ! $is_ipv4 - && function_exists( 'filter_var' ) + && extension_loaded( 'filter' ) && ! filter_var( $host, FILTER_VALIDATE_DOMAIN, diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index e3f18d4d27f89..2215514fc78dc 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -569,6 +569,9 @@ public function data_wp_http_validate_url_should_not_validate() { 'underscore_in_hostname' => array( 'url' => 'https://foo_bar.example.com/', ), + 'valid_ip_host' => array( + 'url' => 'https://1.1.1.1/', + ), ); } From 6c7eccb4851544fc6c11a5c6dcc17b8256461777 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 14:56:01 +0700 Subject: [PATCH 04/11] Tests: fix inline alignment to satisfy PHPCS --- tests/phpunit/tests/http/http.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 2215514fc78dc..46fc849125609 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -566,11 +566,11 @@ 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/', + 'underscore_in_hostname' => array( + 'url' => 'https://foo_bar.example.com/', ), - 'valid_ip_host' => array( - 'url' => 'https://1.1.1.1/', + 'valid_ip_host' => array( + 'url' => 'https://1.1.1.1/', ), ); } From d8d86bdf824fbf375d6bd787642ea256d49cdb14 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 15:13:15 +0700 Subject: [PATCH 05/11] Tests: remove duplicate IP case and fix array formatting --- tests/phpunit/tests/http/http.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 46fc849125609..86b4fbb81a2ad 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -566,11 +566,8 @@ 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/', - ), - 'valid_ip_host' => array( - 'url' => 'https://1.1.1.1/', + 'underscore_in_hostname' => array( + 'url' => 'https://foo_bar.example.com/', ), ); } From aa5990c17e7625a8498f35d4d04b7e135fad6625 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Tue, 30 Dec 2025 15:22:14 +0700 Subject: [PATCH 06/11] Tests: fix array alignment for PHPCS --- tests/phpunit/tests/http/http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 86b4fbb81a2ad..e5acd3a518982 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -566,7 +566,7 @@ 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( + 'underscore_in_hostname' => array( 'url' => 'https://foo_bar.example.com/', ), ); From fe877fdea07f0cda13b8ecdc233ba0958d19a5b2 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Wed, 31 Dec 2025 09:39:13 +0700 Subject: [PATCH 07/11] HTTP: compute IPv4 host earlier --- src/wp-includes/http.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index c1bf29f41ddbf..defb13249e3a4 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -587,13 +587,12 @@ function wp_http_validate_url( $url ) { $parsed_home = parse_url( get_option( 'home' ) ); $same_host = isset( $parsed_home['host'] ) && strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] ); $host = trim( $parsed_url['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 + ); if ( ! $same_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 - ); - if ( ! $is_ipv4 && extension_loaded( 'filter' ) From 0ad1fab7227a8c217007c74faeb90449d934b081 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Fri, 2 Jan 2026 07:32:25 +0700 Subject: [PATCH 08/11] Update http.php Co-authored-by: Weston Ruter --- src/wp-includes/http.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index defb13249e3a4..b947354e3fbc9 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -594,9 +594,9 @@ function wp_http_validate_url( $url ) { if ( ! $same_host ) { if ( - ! $is_ipv4 - && extension_loaded( 'filter' ) - && ! filter_var( + ! $is_ipv4 && + extension_loaded( 'filter' ) && + ! filter_var( $host, FILTER_VALIDATE_DOMAIN, array( 'flags' => FILTER_FLAG_HOSTNAME ) From 889fe26450dbc1b0ec688c803f632d7913ad910e Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Fri, 2 Jan 2026 07:43:21 +0700 Subject: [PATCH 09/11] HTTP: use strict false check for filter_var() --- src/wp-includes/http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index b947354e3fbc9..f4a1f44604ccd 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -596,7 +596,7 @@ function wp_http_validate_url( $url ) { if ( ! $is_ipv4 && extension_loaded( 'filter' ) && - ! filter_var( + false === filter_var( $host, FILTER_VALIDATE_DOMAIN, array( 'flags' => FILTER_FLAG_HOSTNAME ) From 897b851cfcb524877109809892a83e3acf139ee7 Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Fri, 2 Jan 2026 10:14:27 +0700 Subject: [PATCH 10/11] HTTP: avoid early invalid-host return for IPv4 addresses --- src/wp-includes/http.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index f4a1f44604ccd..1ceade9b8d77d 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -587,24 +587,21 @@ function wp_http_validate_url( $url ) { $parsed_home = parse_url( get_option( 'home' ) ); $same_host = isset( $parsed_home['host'] ) && strtolower( $parsed_home['host'] ) === strtolower( $parsed_url['host'] ); $host = trim( $parsed_url['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 - ); if ( ! $same_host ) { if ( - ! $is_ipv4 && extension_loaded( 'filter' ) && false === filter_var( $host, FILTER_VALIDATE_DOMAIN, array( 'flags' => FILTER_FLAG_HOSTNAME ) - ) + ) && + ! 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 ) ) { return false; } - if ( $is_ipv4 ) { + + 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 ) ) { $ip = $host; } else { $ip = gethostbyname( $host ); From 9f70512da3f5f7fa550b265f3cfb7b8c3687b20a Mon Sep 17 00:00:00 2001 From: Phuc Nguyen Date: Mon, 5 Jan 2026 14:29:50 +0700 Subject: [PATCH 11/11] HTTP: Avoid rejecting hosts with underscore subdomains --- src/wp-includes/http.php | 51 ++++++++++++++++++++++++------- tests/phpunit/tests/http/http.php | 36 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index 1ceade9b8d77d..9b8f3bd648ffd 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -589,19 +589,48 @@ function wp_http_validate_url( $url ) { $host = trim( $parsed_url['host'], '.' ); if ( ! $same_host ) { - if ( - extension_loaded( 'filter' ) && - false === filter_var( - $host, - FILTER_VALIDATE_DOMAIN, - array( 'flags' => FILTER_FLAG_HOSTNAME ) - ) && - ! 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 ) - ) { - return false; + $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 ( 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 ) ) { + 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 e5acd3a518982..3c61cc4aba4ff 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -716,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 ); + } + } }