From 1929ab8fcbf71a6e68c791fafb4743a521a6ec74 Mon Sep 17 00:00:00 2001 From: sanketio Date: Tue, 13 Jan 2026 15:27:47 +0530 Subject: [PATCH 1/3] Fix unnecessary 301 canonical redirect for query string encoding --- src/wp-includes/canonical.php | 19 +++++++++ tests/phpunit/tests/canonical.php | 64 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9315ba7fb7ff9..9e8b76a172a07 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -774,6 +774,25 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { return; } + /* + * Avoid redirects when URLs differ only in query string encoding. + * Per RFC 3986, certain characters can be represented in multiple equivalent ways: + * - Spaces: '+' vs '%20' (e.g., ?name=John+Doe vs ?name=John%20Doe) + * - Unreserved chars unnecessarily encoded: '~' vs '%7E', '-' vs '%2D', '_' vs '%5F', '.' vs '%2E' + * - Reserved chars in values: '/' vs '%2F', ':' vs '%3A', '@' vs '%40' + * + * Example problematic scenarios: + * - UTM params: ?utm_content=Hello+World vs ?utm_content=Hello%20World + * - Encoded paths: ?redirect=/path/to/page vs ?redirect=%2Fpath%2Fto%2Fpage + * - Email params: ?email=user@example.com vs ?email=user%40example.com + * + * Redirecting between these variants provides no SEO or functional benefit + * while potentially causing caching issues and breaking analytics. + */ + if ( $redirect_url && urldecode( $redirect_url ) === urldecode( $requested_url ) ) { + return; + } + // Hex-encoded octets are case-insensitive. if ( str_contains( $requested_url, '%' ) ) { if ( ! function_exists( 'lowercase_octets' ) ) { diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 886b09312910e..9d60ab10d4374 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -263,6 +263,12 @@ public function data_canonical() { array( '/2008%20', '/2008' ), array( '//2008////', '/2008/' ), + // Query string encoding variants should not redirect (Ticket #64376). + array( '/?test=one+two', '/?test=one+two' ), // Plus sign should not redirect to %20. + array( '/?test=one%20two', '/?test=one%20two' ), // %20 should not redirect to plus. + array( '/?email=user%40example.com', '/?email=user%40example.com' ), // Encoded @ should not redirect. + array( '/?redirect=%2Fpath%2Fto%2Fpage', '/?redirect=%2Fpath%2Fto%2Fpage' ), // Encoded slashes should not redirect. + // @todo Endpoints (feeds, trackbacks, etc). More fuzzed mixed query variables, comment paging, Home page (static). ); } @@ -465,6 +471,64 @@ public function test_feed_canonical_with_not_exists_query() { $this->assertNull( $redirect ); } + /** + * Test that query string encoding variants do not trigger redirects. + * + * Ensures that URLs differing only in encoding (e.g., '+' vs '%20' for spaces) + * do not cause unnecessary 301 redirects. + * + * @ticket 64376 + */ + public function test_query_string_encoding_variants_no_redirect() { + // Create a static front page to match the original bug report scenario. + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + ) + ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $page_id ); + + // Test 1: Plus signs in UTM parameters should not redirect to %20. + $url_with_plus = home_url( '/?utm_content=Hello+World' ); + $url_with_percent = home_url( '/?utm_content=Hello%20World' ); + + $this->go_to( $url_with_plus ); + $redirect_from_plus = redirect_canonical( $url_with_plus, false ); + + $this->go_to( $url_with_percent ); + $redirect_from_percent = redirect_canonical( $url_with_percent, false ); + + // Both should return false (no redirect). + $this->assertFalse( $redirect_from_plus, 'URL with + should not redirect' ); + $this->assertFalse( $redirect_from_percent, 'URL with %20 should not redirect' ); + + // Test 2: Encoded @ symbol in email parameters. + $url_encoded_at = home_url( '/?email=user%40example.com' ); + + $this->go_to( $url_encoded_at ); + $redirect = redirect_canonical( $url_encoded_at, false ); + $this->assertFalse( $redirect, 'URL with encoded @ should not redirect' ); + + // Test 3: Encoded forward slashes in redirect parameters. + $url_encoded_slash = home_url( '/?redirect=%2Fpath%2Fto%2Fpage' ); + + $this->go_to( $url_encoded_slash ); + $redirect = redirect_canonical( $url_encoded_slash, false ); + $this->assertFalse( $redirect, 'URL with encoded slashes should not redirect' ); + + // Test 4: Multiple query parameters with mixed encoding. + $url_mixed = home_url( '/?name=John+Doe&city=New+York&zip=12345' ); + + $this->go_to( $url_mixed ); + $redirect = redirect_canonical( $url_mixed, false ); + $this->assertFalse( $redirect, 'URL with multiple plus-encoded parameters should not redirect' ); + + // Clean up. + delete_option( 'page_on_front' ); + delete_option( 'show_on_front' ); + } + /** * Test canonical redirects for attachment pages when the option is disabled. * From b111c0234c98f61d3cdc179b36d0bb72273245e1 Mon Sep 17 00:00:00 2001 From: sanketio Date: Tue, 13 Jan 2026 18:50:15 +0530 Subject: [PATCH 2/3] Fix assertions --- tests/phpunit/tests/canonical.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/canonical.php b/tests/phpunit/tests/canonical.php index 9d60ab10d4374..e8f5aa29c95e8 100644 --- a/tests/phpunit/tests/canonical.php +++ b/tests/phpunit/tests/canonical.php @@ -499,30 +499,30 @@ public function test_query_string_encoding_variants_no_redirect() { $this->go_to( $url_with_percent ); $redirect_from_percent = redirect_canonical( $url_with_percent, false ); - // Both should return false (no redirect). - $this->assertFalse( $redirect_from_plus, 'URL with + should not redirect' ); - $this->assertFalse( $redirect_from_percent, 'URL with %20 should not redirect' ); + // Both should return null (no redirect). + $this->assertNull( $redirect_from_plus, 'URL with + should not redirect' ); + $this->assertNull( $redirect_from_percent, 'URL with %20 should not redirect' ); // Test 2: Encoded @ symbol in email parameters. $url_encoded_at = home_url( '/?email=user%40example.com' ); $this->go_to( $url_encoded_at ); $redirect = redirect_canonical( $url_encoded_at, false ); - $this->assertFalse( $redirect, 'URL with encoded @ should not redirect' ); + $this->assertNull( $redirect, 'URL with encoded @ should not redirect' ); // Test 3: Encoded forward slashes in redirect parameters. $url_encoded_slash = home_url( '/?redirect=%2Fpath%2Fto%2Fpage' ); $this->go_to( $url_encoded_slash ); $redirect = redirect_canonical( $url_encoded_slash, false ); - $this->assertFalse( $redirect, 'URL with encoded slashes should not redirect' ); + $this->assertNull( $redirect, 'URL with encoded slashes should not redirect' ); // Test 4: Multiple query parameters with mixed encoding. $url_mixed = home_url( '/?name=John+Doe&city=New+York&zip=12345' ); $this->go_to( $url_mixed ); $redirect = redirect_canonical( $url_mixed, false ); - $this->assertFalse( $redirect, 'URL with multiple plus-encoded parameters should not redirect' ); + $this->assertNull( $redirect, 'URL with multiple plus-encoded parameters should not redirect' ); // Clean up. delete_option( 'page_on_front' ); From 5e3b2be4b4b843d5d4979a83706d630e39b6c4bf Mon Sep 17 00:00:00 2001 From: sanketio Date: Fri, 16 Jan 2026 10:03:40 +0530 Subject: [PATCH 3/3] Fix encoded URL condition --- src/wp-includes/canonical.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/canonical.php b/src/wp-includes/canonical.php index 9e8b76a172a07..d68e02b81c676 100644 --- a/src/wp-includes/canonical.php +++ b/src/wp-includes/canonical.php @@ -789,7 +789,7 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) { * Redirecting between these variants provides no SEO or functional benefit * while potentially causing caching issues and breaking analytics. */ - if ( $redirect_url && urldecode( $redirect_url ) === urldecode( $requested_url ) ) { + if ( urldecode( $redirect_url ) === urldecode( $requested_url ) ) { return; }