From f797ee0ce1b69de0a14bf5713dffe06a0c9d1579 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Wed, 26 Feb 2025 18:20:00 -0500 Subject: [PATCH 1/6] **Overall Goal** The primary goal of this diff is to enhance the `sanitize_term()` function and related code to make the sanitization of term data more robust, consistent, and efficient. It also fixes several bugs and edge cases in how terms were sanitized in various contexts. **Key Changes** 1. **`sanitize_term()` Enhancements:** * **Context-Specific Sanitization:** The core of the changes revolves around ensuring that terms are correctly sanitized based on the specified `$context` ('edit', 'db', 'display', 'rss', etc.). * **Filter Tracking:** A new `filter` property is added to the term object (or array) to keep track of the context in which it was last sanitized. This helps prevent redundant sanitization and ensures the correct filters are applied. * **Object/Array Handling** The code has been updated to handle both array and object more carefully. * **Raw Context is now handled by WP_Term:** The new function `WP_Term->filter('raw')` now properly returns the raw term data. * **Slug Sanitization**: The slug is now sanitized using sanitize_title in multiple contexts. * **Description Sanitization in 'db' context:** The Description is now more carefully sanitized in 'db' context, it is not simply stripped of HTML, but content in scripts is removed. * **Sanitization in display context:** The code now escapes html when using the display context. * **Sanitization in 'rss' context:** The code has been modified to stop stripping html when in rss context. * **Improved Code Clarity:** The code within `sanitize_term()` and `sanitize_term_field()` is more organized and easier to understand. 2. **`sanitize_term_field()` Refinements:** * **Context-Aware Filters:** The logic for applying filters in `sanitize_term_field()` is improved to apply the appropriate filters for each context. * **'db' Context:** The 'db' context now more aggressively strips HTML and potentially harmful content. * **'edit' Context:** The 'edit' context HTML-encodes data to make it safe for display in input fields. * **'display' Context:** Now does html escaping. * **Slug Sanitization** The slug is now sanitized in all contexts, which should help prevent a wide range of unexpected data. * **Rss Context**: Html is not stripped when in this context. 3. **`WP_Term` Class Changes:** * **`filter()` Method:** The `WP_Term` class now has a dedicated `filter()` method to apply sanitization to a `WP_Term` object. * **`get_instance()` Improvements:** The logic in `get_instance()` is updated to handle term objects with different filter states. * **Filter Property**: This property now accurately reflects the state of the object. 4. **New Unit Tests:** * **`Tests_Term_SanitizeTerm`:** A comprehensive test suite is added to cover various sanitization scenarios, including different contexts, input types, and potential edge cases. This ensures that the changes are well-tested and don't introduce regressions. 5. **Deprecation of `sanitize_category` and `sanitize_category_field`:** * These functions are now deprecated in favor of the more general `sanitize_term` and `sanitize_term_field`. * The functions have been moved to deprecated.php. 6. **Remove redundant sanitization**: `WP_Term->filter()` now checks the current filter, to ensure that the same filters are not applied multiple times. 7. **Fix bug**: An edge case where the slug was not being sanitized when dealing with objects. **Potential Implications** * **Enhanced Security:** The more rigorous sanitization, especially in the 'db' and 'edit' contexts, significantly improves security by preventing XSS vulnerabilities and data corruption. * **Improved Data Integrity:** Sanitizing the slug more thoroughly across contexts enhances data integrity and consistency. * **Less Unexpected Behavior:** The clear differentiation between contexts should result in more predictable behavior when working with term data. * **Code Maintainability:** The code structure is improved, making it easier to maintain and extend in the future. * **Breaking changes:** Deprecating `sanitize_category` and `sanitize_category_field` means that developers will need to start updating their code to use `sanitize_term` and `sanitize_term_field`. **Code Review Notes** * **Unit Tests are Excellent:** The new unit tests are thorough and comprehensive. This is a huge plus for ensuring code quality. * **Code Clarity:** The code is generally well-structured and readable. * **Slug Sanitization**: The decision to sanitize the slug in multiple contexts is a good one. * **Description Sanitization**: The changes made to the sanitization of the description is a big improvement. * **Rss context:** The changes made to the sanitization in the rss context is a big improvement. **In summary** This diff is a substantial improvement to term sanitization in WordPress. It addresses several security concerns, enhances data integrity, and makes the code more robust and maintainable. The added unit tests are a significant contribution to the project. I highly recommend merging these changes. --- src/wp-includes/category.php | 28 - src/wp-includes/class-wp-term.php | 16 +- src/wp-includes/deprecated.php | 34 + src/wp-includes/taxonomy.php | 52 +- tests/phpunit/tests/taxonomy/sanitizeTerm.php | 930 ++++++++++++++++++ 5 files changed, 1012 insertions(+), 48 deletions(-) create mode 100644 tests/phpunit/tests/taxonomy/sanitizeTerm.php diff --git a/src/wp-includes/category.php b/src/wp-includes/category.php index d1c43274d704b..318cfed74028d 100644 --- a/src/wp-includes/category.php +++ b/src/wp-includes/category.php @@ -249,34 +249,6 @@ function cat_is_ancestor_of( $cat1, $cat2 ) { return term_is_ancestor_of( $cat1, $cat2, 'category' ); } -/** - * Sanitizes category data based on context. - * - * @since 2.3.0 - * - * @param object|array $category Category data. - * @param string $context Optional. Default 'display'. - * @return object|array Same type as $category with sanitized data for safe use. - */ -function sanitize_category( $category, $context = 'display' ) { - return sanitize_term( $category, 'category', $context ); -} - -/** - * Sanitizes data in single category key field. - * - * @since 2.3.0 - * - * @param string $field Category key to sanitize. - * @param mixed $value Category value to sanitize. - * @param int $cat_id Category ID. - * @param string $context What filter to use, 'raw', 'display', etc. - * @return mixed Value after $value has been sanitized. - */ -function sanitize_category_field( $field, $value, $cat_id, $context ) { - return sanitize_term_field( $field, $value, $cat_id, 'category', $context ); -} - /* Tags */ /** diff --git a/src/wp-includes/class-wp-term.php b/src/wp-includes/class-wp-term.php index 0f5631353e876..a624ef10c1d2d 100644 --- a/src/wp-includes/class-wp-term.php +++ b/src/wp-includes/class-wp-term.php @@ -181,10 +181,7 @@ public static function get_instance( $term_id, $taxonomy = null ) { } } - $term_obj = new WP_Term( $_term ); - $term_obj->filter( $term_obj->filter ); - - return $term_obj; + return new WP_Term( $_term ); } /** @@ -208,7 +205,16 @@ public function __construct( $term ) { * @param string $filter Filter context. Accepts 'edit', 'db', 'display', 'attribute', 'js', 'rss', or 'raw'. */ public function filter( $filter ) { - sanitize_term( $this, $this->taxonomy, $filter ); + if ( $this->filter === $filter ) { + + return $this; + } + if ( 'raw' === $filter ) { + + return self::get_instance( $this->term_id ); + } + + return sanitize_term( $this, $this->taxonomy, $filter ); } /** diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index a47e7015b1348..ac9aec5e95e61 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -6422,3 +6422,37 @@ function wp_create_block_style_variation_instance_name( $block, $variation ) { function current_user_can_for_blog( $blog_id, $capability, ...$args ) { return current_user_can_for_site( $blog_id, $capability, ...$args ); } + + +/** + * Sanitizes category data based on context. + * + * @since 2.3.0 + * @deprecated 6.9.0 + * + * @param object|array $category Category data. + * @param string $context Optional. Default 'display'. + * @return object|array Same type as $category with sanitized data for safe use. + */ +function sanitize_category( $category, $context = 'display' ) { + _deprecated_function( __FUNCTION__, '6.9.0', 'sanitize_term' ); + return sanitize_term( $category, 'category', $context ); +} + +/** + * Sanitizes data in single category key field. + * + * @since 2.3.0 + * @deprecated 6.9.0 + * + * @param string $field Category key to sanitize. + * @param mixed $value Category value to sanitize. + * @param int $cat_id Category ID. + * @param string $context What filter to use, 'raw', 'display', etc. + * + * @return mixed Value after $value has been sanitized. + */ +function sanitize_category_field( $field, $value, $cat_id, $context ) { + _deprecated_function( __FUNCTION__, '6.9.0', 'sanitize_term_field' ); + return sanitize_term_field( $field, $value, $cat_id, 'category', $context ); +} diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 3e235b780fabb..c04d092774aae 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -986,9 +986,11 @@ function get_term( $term, $taxonomy = '', $output = OBJECT, $filter = 'raw' ) { if ( $term instanceof WP_Term ) { $_term = $term; } elseif ( is_object( $term ) ) { - if ( empty( $term->filter ) || 'raw' === $term->filter ) { + if ( empty( $term->filter ) ) { $_term = sanitize_term( $term, $taxonomy, 'raw' ); $_term = new WP_Term( $_term ); + } elseif ( 'raw' === $term->filter ) { + $_term = new WP_Term( $term ); } else { $_term = WP_Term::get_instance( $term->term_id ); } @@ -1708,32 +1710,42 @@ function term_is_ancestor_of( $term1, $term2, $taxonomy ) { * @param array|object $term The term to check. * @param string $taxonomy The taxonomy name to use. * @param string $context Optional. Context in which to sanitize the term. - * Accepts 'raw', 'edit', 'db', 'display', 'rss', + * Accepts 'edit', 'db', 'display', 'rss', * 'attribute', or 'js'. Default 'display'. * @return array|object Term with all fields sanitized. */ function sanitize_term( $term, $taxonomy, $context = 'display' ) { $fields = array( 'term_id', 'name', 'description', 'slug', 'count', 'parent', 'term_group', 'term_taxonomy_id', 'object_id' ); - $do_object = is_object( $term ); - - $term_id = $do_object ? $term->term_id : ( isset( $term['term_id'] ) ? $term['term_id'] : 0 ); - - foreach ( (array) $fields as $field ) { - if ( $do_object ) { + if ( is_object( $term ) ) { + // Check if term already filtered for this context. + if ( isset( $term->filter ) && $context === $term->filter ) { + return $term; + } + if ( ! isset( $term->term_id ) ) { + $term->term_id = 0; + } + foreach ( (array) $fields as $field ) { if ( isset( $term->$field ) ) { - $term->$field = sanitize_term_field( $field, $term->$field, $term_id, $taxonomy, $context ); + + $term->$field = sanitize_term_field( $field, $term->$field, $term->term_id, $taxonomy, $context ); } - } else { + } + $term->filter = $context; + } elseif ( is_array( $term ) ) { + // Check if term already filtered for this context. + if ( isset( $term['filter'] ) && $context === $term['filter'] ) { + return $term; + } + if ( ! isset( $term['term_id'] ) ) { + $term['term_id'] = 0; + } + foreach ( (array) $fields as $field ) { if ( isset( $term[ $field ] ) ) { - $term[ $field ] = sanitize_term_field( $field, $term[ $field ], $term_id, $taxonomy, $context ); + $term[ $field ] = sanitize_term_field( $field, $term[ $field ], $term['term_id'], $taxonomy, $context ); } } - } - if ( $do_object ) { - $term->filter = $context; - } else { $term['filter'] = $context; } @@ -1809,6 +1821,8 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { if ( 'description' === $field ) { $value = esc_html( $value ); // textarea_escaped + } elseif ( 'slug' === $field ) { + $value = sanitize_title( $value ); } else { $value = esc_attr( $value ); } @@ -1876,6 +1890,10 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { * @param mixed $value Value of the taxonomy field. */ $value = apply_filters( "{$taxonomy}_{$field}_rss", $value ); + + if ( 'slug' === $field ) { + $value = sanitize_title( $value ); + } } else { // Use display filters by default. @@ -1906,6 +1924,10 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { * @param string $context Context to retrieve the taxonomy field value. */ $value = apply_filters( "{$taxonomy}_{$field}", $value, $term_id, $context ); + + if ( 'slug' === $field ) { + $value = sanitize_title( $value ); + } } if ( 'attribute' === $context ) { diff --git a/tests/phpunit/tests/taxonomy/sanitizeTerm.php b/tests/phpunit/tests/taxonomy/sanitizeTerm.php new file mode 100644 index 0000000000000..e1f71d2415b28 --- /dev/null +++ b/tests/phpunit/tests/taxonomy/sanitizeTerm.php @@ -0,0 +1,930 @@ +term->create( array( 'taxonomy' => self::$taxonomy ) ); + } + + /** + * Unregister taxonomy after tests. + */ + public static function wpTearDownAfterClass() { + unregister_taxonomy( self::$taxonomy ); + } + + /** + * Test sanitize_term() with an invalid context. + * + * @ticket 50568 + */ + public function test_sanitize_term_invalid_context() { + $t = array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ); + + $expected = $t; + $expected['filter'] = 'invalid-context'; + $this->assertSame( sanitize_term( $t, self::$taxonomy, 'invalid-context' ), $expected ); + } + + /** + * Tests sanitize_term() with 'edit' context. + * + * @ticket 50568 + * + * @dataProvider data_sanitize_term_edit + * + * @param array $term_data Term data to sanitize. + * @param array $expected Expected term data. + */ + public function test_sanitize_term_edit( $term_data, $expected ) { + + $taxonomy = self::$taxonomy; + // array in array out + $actual = sanitize_term( $term_data, $taxonomy, 'edit' ); + $this->assertSame( $expected, $actual ); + + // Object in object out + $term = (object) $term_data; + $expected_oject = (object) $expected; + $actual = sanitize_term( $term, $taxonomy, 'edit' ); + $this->assertEquals( $expected_oject, $actual ); + } + + /** + * Data provider for test_sanitize_term_edit(). + * + * @ticket 50568 + * + * @return array + */ + public function data_sanitize_term_edit() { + return array( + 'valid term' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'name with html' => array( + array( + 'term_id' => 1, + 'name' => '

My Term

', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '<p>My Term</p>', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'name with javascript' => array( + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '<script>alert("XSS")</script>', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'slug with uppercase' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-TERM', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'slug with special char' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-!@#$%', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'description with script' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '<script>alert("XSS")</script>', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + 'description with html' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '<p>My description</p>', + 'parent' => 0, + 'count' => 0, + 'filter' => 'edit', + ), + ), + + ); + } + + /** + * Tests sanitize_term() with 'edit' context. + * + * @ticket 50568 + * + * @dataProvider data_sanitize_term_db + * + * @param array $term_data Term data to sanitize. + * @param array $expected Expected term data. + */ + public function test_sanitize_term_db( $term_data, $expected ) { + $taxonomy = self::$taxonomy; + // array in array out + $actual = sanitize_term( $term_data, $taxonomy, 'db' ); + $this->assertSame( $expected, $actual ); + + // Object in object out + $term = (object) $term_data; + $expected_oject = (object) $expected; + $actual = sanitize_term( $term, $taxonomy, 'db' ); + $this->assertEquals( $expected_oject, $actual ); + } + + /** + * Data provider for test_sanitize_term_db(). + * + * @ticket 50568 + * + * @return array + */ + public function data_sanitize_term_db() { + return array( + 'valid term' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'name with html' => array( + array( + 'term_id' => 1, + 'name' => '

My Term

', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'name with javascript' => array( + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'slug with uppercase' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-TERM', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'slug with special char' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-!@#$%', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'description with script' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => 'alert(\"XSS\")', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + 'description with html' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => 'My description', + 'parent' => 0, + 'count' => 0, + 'filter' => 'db', + ), + ), + + ); + } + + /** + * Tests sanitize_term() with 'display' context. + * + * @ticket 50568 + * + * @dataProvider data_sanitize_term_rss + * + * @param array $term_data Term data to sanitize. + * @param array $expected Expected term data. + */ + public function test_sanitize_term_rss( $term_data, $expected ) { + $taxonomy = self::$taxonomy; + // array in array out + $actual = sanitize_term( $term_data, $taxonomy, 'rss' ); + $this->assertSame( $expected, $actual ); + + // Object in object out + $term = (object) $term_data; + $expected_oject = (object) $expected; + $actual = sanitize_term( $term, $taxonomy, 'rss' ); + $this->assertEquals( $expected_oject, $actual ); + } + + /** + * Data provider for test_sanitize_term_rss(). + * + * @return array + */ + public function data_sanitize_term_rss() { + return array( + 'valid term' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'name with html' => array( + array( + 'term_id' => 1, + 'name' => '

My Term

', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '

My Term

', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'name with javascript' => array( + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'slug with uppercase' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-TERM', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'slug with special char' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-!@#$%', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'description with script' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + 'description with html' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

', + 'parent' => 0, + 'count' => 0, + 'filter' => 'rss', + ), + ), + + ); + } + + /** + * Tests sanitize_term() with 'display' context. + * + * @ticket 50568 + * + * @dataProvider data_sanitize_term_display + * + * @param array $term_data Term data to sanitize. + * @param array $expected Expected term data. + */ + public function test_sanitize_term_display( $term_data, $expected ) { + $taxonomy = self::$taxonomy; + // array in array out + $actual = sanitize_term( $term_data, $taxonomy, 'display' ); + $this->assertSame( $expected, $actual ); + + // Object in object out + $term = (object) $term_data; + $expected_oject = (object) $expected; + $actual = sanitize_term( $term, $taxonomy, 'display' ); + $this->assertEquals( $expected_oject, $actual ); + } + + /** + * Data provider for test_sanitize_term_display(). + * + * @return array + */ + public function data_sanitize_term_display() { + return array( + 'valid term' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'name with html' => array( + array( + 'term_id' => 1, + 'name' => '

My Term

', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '<p>My Term</p>', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'name with javascript' => array( + array( + 'term_id' => 1, + 'name' => '', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => '<script>alert("XSS")</script>', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'slug with uppercase' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-TERM', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'slug with special char' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'MY-!@#$%', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'description with script' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

+', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + 'description with html' => array( + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

', + 'parent' => 0, + 'count' => 0, + 'filter' => 'raw', + ), + array( + 'term_id' => 1, + 'name' => 'My Term', + 'slug' => 'my-term', + 'term_group' => 0, + 'term_taxonomy_id' => 1, + 'taxonomy' => self::$taxonomy, + 'description' => '

My description

+', + 'parent' => 0, + 'count' => 0, + 'filter' => 'display', + ), + ), + + ); + } +} From 53b0453413af12a0f5bc201f16d8c7689d6d4687 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Mon, 7 Jul 2025 18:52:35 -0400 Subject: [PATCH 2/6] Sanitize integer term fields in `sanitize_term_field`. --- src/wp-includes/taxonomy.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index d35a69d2ffbeb..6db00e1511db6 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1829,6 +1829,8 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { $value = esc_html( $value ); // textarea_escaped } elseif ( 'slug' === $field ) { $value = sanitize_title( $value ); + } elseif ( in_array( $field, $int_fields, true ) ) { + $value = ( int ) $value; } else { $value = esc_attr( $value ); } From 866792dfeef208cd0ad0b541bf4cdd1afdc98a37 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 29 Jul 2025 17:20:27 -0400 Subject: [PATCH 3/6] Update taxonomy.php white space --- src/wp-includes/taxonomy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 6db00e1511db6..62791b1856e6d 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1830,7 +1830,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { } elseif ( 'slug' === $field ) { $value = sanitize_title( $value ); } elseif ( in_array( $field, $int_fields, true ) ) { - $value = ( int ) $value; + $value = (int) $value; } else { $value = esc_attr( $value ); } From ecdc398308d1f74c88358e1e22f5efff6be022e8 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 29 Jul 2025 17:50:16 -0400 Subject: [PATCH 4/6] Refine integer field sanitization in `sanitize_term_field` by casting to string and using `absint`. --- src/wp-includes/taxonomy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index dfca8d6d8a498..654a1709c9e92 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1830,7 +1830,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { } elseif ( 'slug' === $field ) { $value = sanitize_title( $value ); } elseif ( in_array( $field, $int_fields, true ) ) { - $value = (int) $value; + $value = (string) absint( $value ); } else { $value = esc_attr( $value ); } From f9598233aa08f8fbf0499e969a7193a096f6684e Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 29 Jul 2025 18:04:22 -0400 Subject: [PATCH 5/6] Refine integer field sanitization in `sanitize_term_field` by casting to string and using `absint`. --- src/wp-includes/taxonomy.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 654a1709c9e92..e7e6cbb6d3053 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1830,7 +1830,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { } elseif ( 'slug' === $field ) { $value = sanitize_title( $value ); } elseif ( in_array( $field, $int_fields, true ) ) { - $value = (string) absint( $value ); + $value = (int) $value; } else { $value = esc_attr( $value ); } @@ -1946,7 +1946,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { - $value = (int) $value; + $value = (string) absint( $value ); } return $value; From 87ee867a2705dbad5a5790b3f6c109cde6b1f83b Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Tue, 29 Jul 2025 18:16:27 -0400 Subject: [PATCH 6/6] Refine integer field sanitization in `sanitize_term_field` by casting to string and using `absint`. --- src/wp-includes/taxonomy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index e7e6cbb6d3053..debb78a322d53 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -1946,7 +1946,7 @@ function sanitize_term_field( $field, $value, $term_id, $taxonomy, $context ) { // Restore the type for integer fields after esc_attr(). if ( in_array( $field, $int_fields, true ) ) { - $value = (string) absint( $value ); + $value = absint( $value ); } return $value;