From b5fdb64e27a3d17141fcb900d1c4299185d9b44d Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 20 Oct 2025 22:18:39 -0700 Subject: [PATCH] KSES: Allow all Custom Data Attributes Allow spec-compliant data-attributes in `wp_kses_attr_check()` --- src/wp-includes/kses.php | 33 ++++++----- tests/phpunit/tests/kses.php | 111 +++++++++++++++++++++++++++++++---- 2 files changed, 118 insertions(+), 26 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 35327e1a01cce..076ac7f73bc2d 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1529,24 +1529,27 @@ function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowe $allowed_attr = $allowed_html[ $element_low ]; + /* + * Allow Custom Data Attributes (`data-*`). + * + * When specifying `$allowed_html`, the attribute name should be set as + * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see + * https://www.w3.org/TR/html40/struct/objects.html#adef-data). + * + * Custom data attributes appear on an HTML element in the `dataset` + * property and are available from JavaScript with a transformed name. + * + * @see https://html.spec.whatwg.org/#custom-data-attribute + */ if ( ! isset( $allowed_attr[ $name_low ] ) || '' === $allowed_attr[ $name_low ] ) { - /* - * Allow `data-*` attributes. - * - * When specifying `$allowed_html`, the attribute name should be set as - * `data-*` (not to be mixed with the HTML 4.0 `data` attribute, see - * https://www.w3.org/TR/html40/struct/objects.html#adef-data). - * - * Note: the attribute name should only contain `A-Za-z0-9_-` chars. - */ - if ( str_starts_with( $name_low, 'data-' ) && ! empty( $allowed_attr['data-*'] ) - && preg_match( '/^data-[a-z0-9_-]+$/', $name_low, $match ) - ) { + $dataset_name = wp_js_dataset_name( $name ); + if ( isset( $dataset_name ) && ! empty( $allowed_attr['data-*'] ) ) { /* - * Add the whole attribute name to the allowed attributes and set any restrictions - * for the `data-*` attribute values for the current element. + * The attribute name passed in here is the `data-*` name, or the name in + * the raw HTML. Add it to the set of allowed attributes and adopt the + * restrictions applied to all custom data attributes for the element. */ - $allowed_attr[ $match[0] ] = $allowed_attr['data-*']; + $allowed_attr[ $name_low ] = $allowed_attr['data-*']; } else { $name = ''; $value = ''; diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 3384a6f137e81..c210de44fe489 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1441,25 +1441,114 @@ public function data_safecss_filter_attr() { /** * Data attributes are globally accepted. * - * @ticket 33121 + * @ticket 61501 + * + * @dataProvider data_data_attributes_and_whether_they_are_allowed + * + * @param string $attribute_name Custom data attribute, e.g. "data-wp-bind--enabled". + * @param bool $is_allowed Whether the given attribute should be allowed. */ - public function test_wp_kses_attr_data_attribute_is_allowed() { - $test = '
Pens and pencils
'; - $expected = '
Pens and pencils
'; + public function test_wp_kses_attr_boolean_data_attribute_is_allowed( string $attribute_name, bool $is_allowed ) { + $element = "
Pens and pencils.
"; - $this->assertEqualHTML( $expected, wp_kses_post( $test ) ); + $processor = new WP_HTML_Tag_Processor( $element ); + $processor->next_tag(); + + $this->assertTrue( + $processor->get_attribute( $attribute_name ), + "Failed to find expected attribute '{$attribute_name}' before filtering: check test." + ); + + $processor = new WP_HTML_Tag_Processor( wp_kses_post( $element ) ); + $this->assertTrue( + $processor->next_tag(), + 'Failed to find containing tag after filtering: check test.' + ); + + if ( $is_allowed ) { + $this->assertTrue( + $processor->get_attribute( $attribute_name ), + "Allowed custom data attribute '{$attribute_name}' should not have been removed." + ); + } else { + $this->assertNull( + $processor->get_attribute( $attribute_name ), + "Should have removed un-allowed custom data attribute '{$attribute_name}'." + ); + } } /** - * Data attributes with leading, trailing, and double "-" are globally accepted. + * Ensures that only allowable custom data attributes with values are retained. + * + * @ticket 33121 + * + * @dataProvider data_data_attributes_and_whether_they_are_allowed * - * @ticket 61052 + * @param string $attribute_name Custom data attribute, e.g. "dat-wp-bind--enabled". + * @param bool $is_allowed Whether the given attribute should be allowed. */ - public function test_wp_kses_attr_data_attribute_hypens_allowed() { - $test = '
Pens and pencils
'; - $expected = '
Pens and pencils
'; + public function test_wp_kses_attr_data_attribute_is_allowed( string $attribute_name, bool $is_allowed ) { + $element = "
Pens and pencils.
"; - $this->assertEqualHTML( $expected, wp_kses_post( $test ) ); + $processor = new WP_HTML_Tag_Processor( $element ); + $processor->next_tag(); + + $this->assertIsString( + $processor->get_attribute( $attribute_name ), + "Failed to find expected attribute '{$attribute_name}' before filtering: check test." + ); + + $processor = new WP_HTML_Tag_Processor( wp_kses_post( $element ) ); + $this->assertTrue( + $processor->next_tag(), + 'Failed to find containing tag after filtering: check test.' + ); + + if ( $is_allowed ) { + $this->assertIsString( + $processor->get_attribute( $attribute_name ), + "Allowed custom data attribute '{$attribute_name}' should not have been removed." + ); + } else { + $this->assertNull( + $processor->get_attribute( $attribute_name ), + "Should have removed un-allowed custom data attribute '{$attribute_name}'." + ); + } + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_data_attributes_and_whether_they_are_allowed() { + return array( + // Allowable custom data attributes. + 'Normative attribute' => array( 'data-foo', true ), + 'Non-consecutive dashes' => array( 'data-two-hyphens', true ), + 'Double-dashes' => array( 'data--double-dash', true ), + 'Trailing dash' => array( 'data-trailing-dash-', true ), + 'Uppercase alphas' => array( 'data-Post-ID', true ), + 'Bind Directive' => array( 'data-wp-bind--enabled', true ), + 'Single-dash suffix' => array( 'data-after-', true ), + 'Double-dash prefix' => array( 'data--before', true ), + 'Double-dash suffix' => array( 'data-after--', true ), + 'Double-dashes everywhere' => array( 'data--one--two--', true ), + 'Underscore' => array( 'data-over_under', true ), + + // Not custom data attributes. + 'No data- prefix' => array( 'post-id', false ), + 'No dash after prefix' => array( 'datainvalid', false ), + + // Un-allowable custom data attributes. + 'Nothing after prefix' => array( 'data-', false ), + 'Whitespace after prefix' => array( "data-\u{2003}", false ), + 'Emoji in name' => array( 'data-🐄', false ), + 'Brackets' => array( 'data-[enabled]', false ), + 'Colon' => array( 'data-wp:bind', false ), + ); } /**