From 34ffbd32023f7c7e1e143fda5febf5c48a765ba3 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Fri, 17 Apr 2026 12:39:05 +0530 Subject: [PATCH 1/2] Fix: Preserve bare < characters in feed titles --- src/wp-includes/default-filters.php | 6 +++-- src/wp-includes/formatting.php | 23 +++++++++++++++++ tests/phpunit/tests/feed/atom.php | 26 ++++++++++++++++++++ tests/phpunit/tests/feed/rss2.php | 38 +++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 4b6d9de25fa11..f08b3f9b50896 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -254,9 +254,11 @@ add_filter( 'wp_get_custom_css', 'wp_replace_insecure_home_url' ); // RSS filters. -add_filter( 'the_title_rss', 'strip_tags' ); + add_filter( 'the_title_rss', 'ent2ncr', 8 ); -add_filter( 'the_title_rss', 'esc_html' ); +add_filter( 'the_title_rss', 'wp_encode_bare_lt', 9 ); +add_filter( 'the_title_rss', 'strip_tags', 10 ); +add_filter( 'the_title_rss', 'esc_html', 11 ); add_filter( 'the_content_rss', 'ent2ncr', 8 ); add_filter( 'the_content_feed', 'wp_staticize_emoji' ); add_filter( 'the_content_feed', '_oembed_filter_feed_content' ); diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 498d676f5c20f..0e1664e78e7b6 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5509,6 +5509,29 @@ function normalize_whitespace( $str ) { return $str; } +/** + * Encodes bare `<` characters (those not starting an HTML tag) as `<`. + * + * @since x.x.x + * + * @param string $text The text to encode. + * @return string Text with bare `<` characters encoded. + */ +function wp_encode_bare_lt( $text ) { + /* + * A valid HTML tag begins with `<` followed by: + * - a letter (opening tag, e.g. ``) + * - `/` (closing tag, e.g. ``) + * - `!` (comment or doctype, e.g. ``) + * - `?` (processing instruction, e.g. ``) + * + * Any `<` not followed by one of these is a bare less-than sign and is + * encoded as `<` so that strip_tags() does not silently discard it. + */ + $encoded = preg_replace( '#<(?![a-zA-Z/!?])#', '<', $text ); + return null !== $encoded ? $encoded : $text; +} + /** * Properly strips all HTML tags including 'script' and 'style'. * diff --git a/tests/phpunit/tests/feed/atom.php b/tests/phpunit/tests/feed/atom.php index 99d4e837afa4a..3cc1baa0d3da4 100644 --- a/tests/phpunit/tests/feed/atom.php +++ b/tests/phpunit/tests/feed/atom.php @@ -294,4 +294,30 @@ public function test_atom_enclosure_with_extended_url_length_type_parsing() { } } } + + /** + * Tests that a bare `<` in a post title is encoded inside the Atom feed CDATA block, not stripped. + * + * Atom wraps `the_title_rss()` output in ``. This test verifies + * the encoded `<` character survives into that block by writing the raw title + * directly to the database, bypassing kses sanitization on insert. + * + * @ticket 9993 + * + * @covers ::get_the_title_rss + */ + public function test_atom_title_encodes_bare_lt_in_cdata() { + global $wpdb; + + $post_id = self::factory()->post->create(); + + $wpdb->update( $wpdb->posts, array( 'post_title' => '& > test <' ), array( 'ID' => $post_id ) ); + clean_post_cache( $post_id ); + + $this->go_to( '/?feed=atom&p=' . $post_id ); + $feed = $this->do_atom(); + + $this->assertStringContainsString( 'test <', $feed, 'Bare `<` was stripped from the Atom feed title CDATA block.' ); + $this->assertStringNotContainsString( 'test ]]>', $feed, 'Bare `<` must not be stripped (leaving title truncated before `]]>`).' ); + } } diff --git a/tests/phpunit/tests/feed/rss2.php b/tests/phpunit/tests/feed/rss2.php index c4f8807d7b7d3..99b74eafebe4e 100644 --- a/tests/phpunit/tests/feed/rss2.php +++ b/tests/phpunit/tests/feed/rss2.php @@ -629,4 +629,42 @@ function ( $headers ) use ( $today ) { $this->go_to( '/?feed=rss2&withcomments=1' ); } + + /** + * Tests that the `the_title_rss` filter encodes bare `<` characters rather than stripping them. + * + * @ticket 9993 + * @dataProvider data_title_rss_encodes_special_characters + * + * @covers ::get_the_title_rss + */ + public function test_title_rss_encodes_special_characters( $title, $expected ) { + $this->assertSame( $expected, apply_filters( 'the_title_rss', $title ) ); + } + + /** + * Data provider for test_title_rss_encodes_special_characters(). + * + * @return array[] + */ + public static function data_title_rss_encodes_special_characters() { + return array( + 'bare less-than at end is encoded not stripped' => array( + '& > test <', + '& > test <', + ), + 'bare less-than in middle is encoded' => array( + 'a < b', + 'a < b', + ), + 'html tags are still stripped by strip_tags' => array( + 'bold', + 'bold', + ), + 'plain text passes through unchanged' => array( + 'Hello World', + 'Hello World', + ), + ); + } } From c92729ef667eb80934105a019127b9bdc877b7fb Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Fri, 17 Apr 2026 12:45:24 +0530 Subject: [PATCH 2/2] Fix PHPCS errors --- tests/phpunit/tests/feed/rss2.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/feed/rss2.php b/tests/phpunit/tests/feed/rss2.php index 99b74eafebe4e..41bd42141e7a3 100644 --- a/tests/phpunit/tests/feed/rss2.php +++ b/tests/phpunit/tests/feed/rss2.php @@ -653,15 +653,15 @@ public static function data_title_rss_encodes_special_characters() { '& > test <', '& > test <', ), - 'bare less-than in middle is encoded' => array( + 'bare less-than in middle is encoded' => array( 'a < b', 'a < b', ), - 'html tags are still stripped by strip_tags' => array( + 'html tags are still stripped by strip_tags' => array( 'bold', 'bold', ), - 'plain text passes through unchanged' => array( + 'plain text passes through unchanged' => array( 'Hello World', 'Hello World', ),