From 2714afca9d367d5e0c75fbb8a28741301bfc8a03 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Sun, 23 Aug 2026 22:25:26 +0530 Subject: [PATCH] fix(rewrite): escape permastruct metacharacters --- src/wp-includes/class-wp-rewrite.php | 15 ++++- tests/phpunit/tests/rewrite.php | 64 +++++++++++++++++++++ tests/phpunit/tests/rewrite/rewriteTags.php | 13 +++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php index 8b75fa5c36d16..22af9fb6c98a9 100644 --- a/src/wp-includes/class-wp-rewrite.php +++ b/src/wp-includes/class-wp-rewrite.php @@ -912,6 +912,11 @@ public function generate_rewrite_rules( $permalink_structure, $ep_mask = EP_NONE $num_tokens = count( $tokens[0] ); + $quoted_rewritecode = array(); + foreach ( $this->rewritecode as $code ) { + $quoted_rewritecode[] = preg_quote( $code, '#' ); + } + $index = $this->index; // Probably 'index.php'. $feedindex = $index; $trackbackindex = $index; @@ -960,7 +965,7 @@ public function generate_rewrite_rules( $permalink_structure, $ep_mask = EP_NONE $struct = ltrim( $struct, '/' ); // Replace tags with regexes. - $match = str_replace( $this->rewritecode, $this->rewritereplace, $struct ); + $match = str_replace( $quoted_rewritecode, $this->rewritereplace, preg_quote( $struct, '#' ) ); // Make a list of tags, and store how many there are in $num_toks. $num_toks = preg_match_all( '/%.+?%/', $struct, $toks ); @@ -1102,8 +1107,12 @@ public function generate_rewrite_rules( $permalink_structure, $ep_mask = EP_NONE // Trim slashes from the end of the regex for this dir. $match = rtrim( $match, '/' ); - // Get rid of brackets. - $submatchbase = str_replace( array( '(', ')' ), '', $match ); + // Remove capture groups from rewrite tag regexes for attachment rules. + $submatchbase = str_replace( + $quoted_rewritecode, + str_replace( array( '(', ')' ), '', $this->rewritereplace ), + preg_quote( rtrim( $struct, '/' ), '#' ) + ); // Add a rule for at attachments, which take the form of /some-text. $sub1 = $submatchbase . '/([^/]+)/'; diff --git a/tests/phpunit/tests/rewrite.php b/tests/phpunit/tests/rewrite.php index 085998e4b7eec..8c6f7596df89e 100644 --- a/tests/phpunit/tests/rewrite.php +++ b/tests/phpunit/tests/rewrite.php @@ -531,6 +531,70 @@ public function test_parse_request_with_post_slug_that_clashes_with_a_trashed_pa $this->assertFalse( is_404() ); } + /** + * @ticket 49510 + */ + public function test_taxonomy_bases_with_regex_metacharacters() { + update_option( 'category_base', 'cat+base' ); + update_option( 'tag_base', 'tag+base' ); + create_initial_taxonomies(); + delete_option( 'rewrite_rules' ); + + $category_id = self::factory()->term->create( + array( + 'name' => 'Category A', + 'taxonomy' => 'category', + ) + ); + $tag_id = self::factory()->term->create( + array( + 'name' => 'Tag A', + 'taxonomy' => 'post_tag', + ) + ); + + $this->go_to( get_term_link( $category_id, 'category' ) ); + $category_name = get_query_var( 'category_name' ); + + $this->go_to( get_term_link( $tag_id, 'post_tag' ) ); + + $this->assertSame( + array( 'category-a', 'tag-a' ), + array( $category_name, get_query_var( 'tag' ) ) + ); + } + + /** + * @ticket 49510 + */ + public function test_taxonomy_base_regex_metacharacter_is_literal() { + update_option( 'category_base', 'cat.base' ); + create_initial_taxonomies(); + delete_option( 'rewrite_rules' ); + + self::factory()->term->create( + array( + 'name' => 'Category A', + 'taxonomy' => 'category', + ) + ); + + $this->go_to( '/catXbase/category-a/' ); + + $this->assertSame( '', get_query_var( 'category_name' ) ); + } + + /** + * @ticket 49510 + */ + public function test_permalink_regex_metacharacters_are_literal_in_attachment_rules() { + global $wp_rewrite; + + $rules = $wp_rewrite->generate_rewrite_rules( '/archive(2026)/%postname%/', EP_PERMALINK ); + + $this->assertSame( 'index.php?attachment=$matches[1]', $rules['archive\(2026\)/[^/]+/attachment/([^/]+)/?$'] ); + } + /** * @ticket 29107 */ diff --git a/tests/phpunit/tests/rewrite/rewriteTags.php b/tests/phpunit/tests/rewrite/rewriteTags.php index 31c28df15e283..57756ac093648 100644 --- a/tests/phpunit/tests/rewrite/rewriteTags.php +++ b/tests/phpunit/tests/rewrite/rewriteTags.php @@ -81,6 +81,19 @@ public function test_add_rewrite_tag_updates_existing() { $this->assertNotContains( 'pagename=', $wp_rewrite->queryreplace ); } + /** + * @ticket 49510 + */ + public function test_generate_rewrite_rules_with_regex_metacharacter_in_tag() { + global $wp_rewrite; + + add_rewrite_tag( '%foo.bar%', '([^/]+)', 'foo=' ); + + $rules = $wp_rewrite->generate_rewrite_rules( '/base/%foo.bar%' ); + + $this->assertSame( 'index.php?foo=$1', $rules['base/([^/]+)/?$'] ); + } + public function test_remove_rewrite_tag() { global $wp_rewrite;