From 74e42816561f8008de62910250547eee9f2f9edf Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:13:41 +0800 Subject: [PATCH 1/3] KSES: Allow CSS color functions in inline styles. `safecss_filter_attr()` rejected any declaration containing a color function such as `rgb()`, `hsl()` or `color-mix()`, so styles like a `box-shadow` with an `rgba()` color were stripped. Color functions are now removed from the test string, innermost first, when their contents are plain color syntax. Anything left over, such as a parenthesis, backslash, comment or unknown function, is still rejected, and the `safecss_filter_attr_allow_css` filter can still reject the declaration. Synced from https://github.com/WordPress/gutenberg/pull/84213. Fixes #66387. --- src/wp-includes/kses.php | 27 +++++- tests/phpunit/tests/kses.php | 169 ++++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index aee33b474a4d6..72189c2891f8b 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -3721,6 +3721,7 @@ function kses_init() { * and URLs in the SVG element reference properties. * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`. * Added support for `background-clip`. + * @since 7.3.0 Added support for CSS color functions, such as `rgb()` and `color-mix()`. * * @param string $css A string of CSS rules, decoded from an HTML `style` attribute. * @param string $deprecated Not used. @@ -4135,7 +4136,31 @@ function safecss_filter_attr( $css, $deprecated = '' ) { } /* - * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc. + * Allow CSS color functions like rgb() and color-mix() by replacing them, innermost first, + * so long as their contents are plain color syntax. The number of passes caps the nesting. + */ + $color_function_regex = '/(? 'height: expression( body.scrollTop + 50 + "px" )', 'expected' => '', ), - // RGB color values are not allowed. + // RGB color values are allowed. array( 'css' => 'color: rgb( 100, 100, 100 )', - 'expected' => '', + 'expected' => 'color: rgb( 100, 100, 100 )', ), - // RGBA color values are not allowed. + // RGBA color values are allowed. array( 'css' => 'color: rgb( 100, 100, 100, .4 )', + 'expected' => 'color: rgb( 100, 100, 100, .4 )', + ), + // Allow rgba() in box-shadow. + array( + 'css' => 'box-shadow: 1px 1px 1px rgba(0,0,0,0.5)', + 'expected' => 'box-shadow: 1px 1px 1px rgba(0,0,0,0.5)', + ), + // Allow color functions in a multi-layer box-shadow. + array( + 'css' => 'box-shadow: 6px 6px 9px rgb(0 0 0 / 20%), 0 0 0 1px hsl(0 0% 0%)', + 'expected' => 'box-shadow: 6px 6px 9px rgb(0 0 0 / 20%), 0 0 0 1px hsl(0 0% 0%)', + ), + // Allow nested color functions. + array( + 'css' => 'color: color-mix(in srgb, rgb(0 0 0) 50%, hsl(0 0% 100%))', + 'expected' => 'color: color-mix(in srgb, rgb(0 0 0) 50%, hsl(0 0% 100%))', + ), + // Allow var() inside a relative color. + array( + 'css' => 'color: rgb(from var(--brand) r g b / 50%)', + 'expected' => 'color: rgb(from var(--brand) r g b / 50%)', + ), + // Allow calc() inside rgb(). + array( + 'css' => 'color: rgb(calc(255 * 0.5) 0 0)', + 'expected' => 'color: rgb(calc(255 * 0.5) 0 0)', + ), + // Allow uppercase color function names. + array( + 'css' => 'color: RGB(0,0,0)', + 'expected' => 'color: RGB(0,0,0)', + ), + // Allow oklch(). + array( + 'css' => 'color: oklch(70% 0.1 200)', + 'expected' => 'color: oklch(70% 0.1 200)', + ), + // Allow hex colors in color-mix(). + array( + 'css' => 'color: color-mix(in srgb, #fff 50%, #000)', + 'expected' => 'color: color-mix(in srgb, #fff 50%, #000)', + ), + // Allow hex colors in light-dark(). + array( + 'css' => 'color: light-dark(#fff, #000)', + 'expected' => 'color: light-dark(#fff, #000)', + ), + // Allow color() with a color space. + array( + 'css' => 'color: color(display-p3 1 0 0)', + 'expected' => 'color: color(display-p3 1 0 0)', + ), + // Color functions may not contain other functions. + array( + 'css' => 'color: rgb(foo(1) 0 0)', + 'expected' => '', + ), + // Color functions may not be wrapped in an unknown function. + array( + 'css' => 'color: foo(rgb(0 0 0))', + 'expected' => '', + ), + // Color functions may not contain a closing curly bracket. + array( + 'css' => 'color: rgb(0 } 0)', + 'expected' => '', + ), + // Color functions may not contain comments. + array( + 'css' => 'color: rgb(0 /* x */ 0)', + 'expected' => '', + ), + // Color functions may not contain a backslash. + array( + 'css' => 'color: rgb(0 \\30 0)', + 'expected' => '', + ), + // Color functions must be closed. + array( + 'css' => 'color: rgb(0 0 0', + 'expected' => '', + ), + // Color function names must match exactly. + array( + 'css' => 'color: x-rgb(0 0 0)', + 'expected' => '', + ), + // Color functions may not be nested beyond the pass limit. + array( + 'css' => 'color: ' . str_repeat( 'rgb(', 40 ) . '0' . str_repeat( ')', 40 ), + 'expected' => '', + ), + // A comment after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) /* x */', + 'expected' => '', + ), + // A backslash after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) \\30', + 'expected' => '', + ), + // A parenthesis after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) (', + 'expected' => '', + ), + // An ampersand after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) &', + 'expected' => '', + ), + // An equals sign after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) =', + 'expected' => '', + ), + // A closing curly bracket after a color function is not allowed. + array( + 'css' => 'color: rgb(0 0 0) }', 'expected' => '', ), // Allow min(). @@ -2343,6 +2464,48 @@ public static function data_safecss_filter_attr_filtered() { ); } + /** + * Tests that a safecss_filter_attr_allow_css filter can reject an allowed color function. + * + * @ticket 66387 + * + * @dataProvider data_safecss_filter_attr_rejecting_filter_priorities + * + * @param int $priority Priority of the rejecting filter. + */ + public function test_safecss_filter_attr_filter_can_reject_color_functions( $priority ) { + add_filter( 'safecss_filter_attr_allow_css', '__return_false', $priority ); + + $result = safecss_filter_attr( 'color: rgb(0 0 0)' ); + + remove_filter( 'safecss_filter_attr_allow_css', '__return_false', $priority ); + + $this->assertSame( '', $result ); + } + + /** + * Data provider for test_safecss_filter_attr_filter_can_reject_color_functions(). + * + * @return array[] + */ + public function data_safecss_filter_attr_rejecting_filter_priorities() { + return array( + 'priority 5' => array( 5 ), + 'priority 10' => array( 10 ), + ); + } + + /** + * Tests that the style engine keeps a box-shadow containing rgba(). + * + * @ticket 66387 + */ + public function test_style_engine_keeps_box_shadow_with_rgba() { + $styles = wp_style_engine_get_styles( array( 'shadow' => '1px 1px 1px rgba(0,0,0,0.5)' ) ); + + $this->assertSame( 'box-shadow:1px 1px 1px rgba(0,0,0,0.5);', $styles['css'] ?? '' ); + } + /** * Test filtering a standard img tag. * From 53e013c6de7e3ec805f8e89be2bde2153c02a957 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:05:23 +0000 Subject: [PATCH 2/3] KSES: Cap CSS color function nesting at 4. Four levels is the deepest real combination of color functions, such as light-dark() around color-mix() around a relative color with an origin color. Explain the cap in a comment and test 4 levels kept and 5 dropped. --- src/wp-includes/kses.php | 9 +++++++-- tests/phpunit/tests/kses.php | 12 +++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 72189c2891f8b..37c8212abc797 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -4137,11 +4137,16 @@ function safecss_filter_attr( $css, $deprecated = '' ) { /* * Allow CSS color functions like rgb() and color-mix() by replacing them, innermost first, - * so long as their contents are plain color syntax. The number of passes caps the nesting. + * so long as their contents are plain color syntax. */ $color_function_regex = '/(? 'color: color(display-p3 1 0 0)', 'expected' => 'color: color(display-p3 1 0 0)', ), + // Allow four levels of nested color functions. + array( + 'css' => 'color: light-dark(color-mix(in srgb, rgb(from hsl(200 50% 40%) r g b) 50%, #fff), #000)', + 'expected' => 'color: light-dark(color-mix(in srgb, rgb(from hsl(200 50% 40%) r g b) 50%, #fff), #000)', + ), + // Allow color functions nested up to the pass limit. + array( + 'css' => 'color: ' . str_repeat( 'rgb(', 4 ) . '0' . str_repeat( ')', 4 ), + 'expected' => 'color: ' . str_repeat( 'rgb(', 4 ) . '0' . str_repeat( ')', 4 ), + ), // Color functions may not contain other functions. array( 'css' => 'color: rgb(foo(1) 0 0)', @@ -1637,7 +1647,7 @@ public static function data_safecss_filter_attr() { ), // Color functions may not be nested beyond the pass limit. array( - 'css' => 'color: ' . str_repeat( 'rgb(', 40 ) . '0' . str_repeat( ')', 40 ), + 'css' => 'color: ' . str_repeat( 'rgb(', 5 ) . '0' . str_repeat( ')', 5 ), 'expected' => '', ), // A comment after a color function is not allowed. From 5510d1c05a970c7006f347ba1020bb71cffacebd Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:50:42 +0800 Subject: [PATCH 3/3] KSES: Simplify the color function filter test and update stale test comments. Test a single rejecting filter at the default priority, since the color function check runs before the filter at any priority. Reword two comments that said RGB and RGBA values need the filter to be allowed. --- tests/phpunit/tests/kses.php | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 15c31c4c7c761..af89d55d7d4e2 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -2461,12 +2461,12 @@ public static function data_safecss_filter_attr_filtered() { 'css' => 'height: expression( body.scrollTop + 50 + "px" )', 'expected' => 'height: expression( body.scrollTop + 50 + "px" )', ), - // RGB color values can be allowed with the 'safecss_filter_attr_allow_css' filter. + // RGB color values are still allowed with the 'safecss_filter_attr_allow_css' filter. array( 'css' => 'color: rgb( 100, 100, 100 )', 'expected' => 'color: rgb( 100, 100, 100 )', ), - // RGBA color values can be allowed with the 'safecss_filter_attr_allow_css' filter. + // RGBA color values are still allowed with the 'safecss_filter_attr_allow_css' filter. array( 'css' => 'color: rgb( 100, 100, 100, .4 )', 'expected' => 'color: rgb( 100, 100, 100, .4 )', @@ -2478,33 +2478,17 @@ public static function data_safecss_filter_attr_filtered() { * Tests that a safecss_filter_attr_allow_css filter can reject an allowed color function. * * @ticket 66387 - * - * @dataProvider data_safecss_filter_attr_rejecting_filter_priorities - * - * @param int $priority Priority of the rejecting filter. */ - public function test_safecss_filter_attr_filter_can_reject_color_functions( $priority ) { - add_filter( 'safecss_filter_attr_allow_css', '__return_false', $priority ); + public function test_safecss_filter_attr_filter_can_reject_color_functions() { + add_filter( 'safecss_filter_attr_allow_css', '__return_false' ); $result = safecss_filter_attr( 'color: rgb(0 0 0)' ); - remove_filter( 'safecss_filter_attr_allow_css', '__return_false', $priority ); + remove_filter( 'safecss_filter_attr_allow_css', '__return_false' ); $this->assertSame( '', $result ); } - /** - * Data provider for test_safecss_filter_attr_filter_can_reject_color_functions(). - * - * @return array[] - */ - public function data_safecss_filter_attr_rejecting_filter_priorities() { - return array( - 'priority 5' => array( 5 ), - 'priority 10' => array( 10 ), - ); - } - /** * Tests that the style engine keeps a box-shadow containing rgba(). *