From 99c52189cecfd91fde32c82f2cc31bc8b55ae17a Mon Sep 17 00:00:00 2001 From: Dhrupo Nil Date: Sat, 27 Jun 2026 13:45:26 +0600 Subject: [PATCH] HTTP: Preserve non-default port in get_allowed_http_origins(). get_allowed_http_origins() built the allowed CORS origin list from the host of admin_url() and home_url() only, dropping any port (there was a '@todo Preserve port?' note). Sites served on a non-default port, such as http://example.com:8080, had their cross-origin requests rejected because the browser Origin header includes the port while the allowed list did not. Append the port to the allowed origins when it is present and non-default. The default HTTP and HTTPS ports (80 and 443) are omitted because browsers leave them out of the Origin header, so an explicit ':80'/':443' in the site URL still produces a port-less origin that matches the request. Adds regression tests covering custom ports, the default-port case, a port-less URL, and is_allowed_http_origin() matching a custom-port origin. Fixes #65522. --- src/wp-includes/http.php | 25 +++- .../tests/http/getAllowedHttpOrigins.php | 121 ++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 tests/phpunit/tests/http/getAllowedHttpOrigins.php diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index 8280f424934dd..3ac500e1de6e9 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -449,13 +449,28 @@ function get_allowed_http_origins() { $admin_origin = parse_url( admin_url() ); $home_origin = parse_url( home_url() ); - // @todo Preserve port? + /* + * Preserve a non-default port so that origins served on a custom port + * (for example `http://example.com:8080`) are matched against the browser's + * `Origin` request header. The default HTTP and HTTPS ports (80 and 443) are + * omitted because browsers leave them out of the `Origin` header. + */ + $admin_host = $admin_origin['host']; + if ( isset( $admin_origin['port'] ) && 80 !== $admin_origin['port'] && 443 !== $admin_origin['port'] ) { + $admin_host .= ':' . $admin_origin['port']; + } + + $home_host = $home_origin['host']; + if ( isset( $home_origin['port'] ) && 80 !== $home_origin['port'] && 443 !== $home_origin['port'] ) { + $home_host .= ':' . $home_origin['port']; + } + $allowed_origins = array_unique( array( - 'http://' . $admin_origin['host'], - 'https://' . $admin_origin['host'], - 'http://' . $home_origin['host'], - 'https://' . $home_origin['host'], + 'http://' . $admin_host, + 'https://' . $admin_host, + 'http://' . $home_host, + 'https://' . $home_host, ) ); diff --git a/tests/phpunit/tests/http/getAllowedHttpOrigins.php b/tests/phpunit/tests/http/getAllowedHttpOrigins.php new file mode 100644 index 0000000000000..a2f7f14106a6d --- /dev/null +++ b/tests/phpunit/tests/http/getAllowedHttpOrigins.php @@ -0,0 +1,121 @@ +original_home = get_option( 'home' ); + $this->original_siteurl = get_option( 'siteurl' ); + } + + public function tear_down() { + update_option( 'home', $this->original_home ); + update_option( 'siteurl', $this->original_siteurl ); + parent::tear_down(); + } + + /** + * A non-default port must be preserved so that an origin served on a custom + * port (for example a local or staging install) is matched. + * + * @ticket 65522 + */ + public function test_non_default_port_is_preserved() { + update_option( 'home', 'http://example.com:8080' ); + update_option( 'siteurl', 'http://example.com:8080' ); + + $origins = get_allowed_http_origins(); + + $this->assertContains( 'http://example.com:8080', $origins ); + $this->assertContains( 'https://example.com:8080', $origins ); + $this->assertNotContains( 'http://example.com', $origins ); + } + + /** + * A URL without a port must not gain a port suffix. + * + * @ticket 65522 + */ + public function test_url_without_port_has_no_port_suffix() { + update_option( 'home', 'http://example.com' ); + update_option( 'siteurl', 'http://example.com' ); + + $origins = get_allowed_http_origins(); + + $this->assertContains( 'http://example.com', $origins ); + $this->assertContains( 'https://example.com', $origins ); + $this->assertNotContains( 'http://example.com:80', $origins ); + } + + /** + * The default HTTP and HTTPS ports must be omitted, because browsers leave + * them out of the `Origin` request header. An explicit `:80` (or `:443`) in + * the site URL must therefore still produce a port-less origin so the check + * matches the value the browser actually sends. + * + * @ticket 65522 + * + * @dataProvider data_default_ports + * + * @param string $url The site URL with an explicit default port. + */ + public function test_default_ports_are_omitted( $url ) { + update_option( 'home', $url ); + update_option( 'siteurl', $url ); + + $origins = get_allowed_http_origins(); + + $this->assertContains( 'http://example.com', $origins, 'A port-less HTTP origin should be present.' ); + $this->assertContains( 'https://example.com', $origins, 'A port-less HTTPS origin should be present.' ); + $this->assertNotContains( 'http://example.com:80', $origins, 'The default HTTP port should be omitted.' ); + $this->assertNotContains( 'https://example.com:443', $origins, 'The default HTTPS port should be omitted.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_default_ports() { + return array( + 'explicit default HTTP port' => array( 'http://example.com:80' ), + 'explicit default HTTPS port' => array( 'https://example.com:443' ), + ); + } + + /** + * A custom port on the site URL must make a matching origin allowed. + * + * @ticket 65522 + * + * @covers ::is_allowed_http_origin + */ + public function test_is_allowed_http_origin_matches_custom_port() { + update_option( 'home', 'http://example.com:8080' ); + update_option( 'siteurl', 'http://example.com:8080' ); + + $this->assertSame( 'http://example.com:8080', is_allowed_http_origin( 'http://example.com:8080' ) ); + $this->assertSame( '', is_allowed_http_origin( 'http://example.com' ), 'A port-less origin should not match a custom-port site.' ); + } +}