From f3601cdfbc40b2a4717e0268e0e7880dc2dd3553 Mon Sep 17 00:00:00 2001 From: Anupkankale Date: Sun, 9 Aug 2026 17:59:27 +0000 Subject: [PATCH] Formatting: match CSS function names case-insensitively in safecss_filter_attr(). CSS function names are ASCII case-insensitive, so `CALC(1px + 2px)`, `URL(foo.jpg)`, and `LINEAR-GRADIENT(red, blue)` are all valid CSS. Every function-matching pattern in `safecss_filter_attr()` was case-sensitive, however, so valid declarations using anything other than all-lowercase function names were silently dropped. Add the `i` modifier to the `url()`, gradient, and generic function patterns, and lowercase the value before the `url(` / `-gradient(` sniffing done for custom properties. Only the function names are matched loosely; the grammar each pattern accepts is unchanged, so a mixed-case value is now held to exactly the same standard as its lowercase equivalent. Protocol checking in `wp_kses_bad_protocol()` was already case-insensitive, so `URL()` values are validated the same way `url()` values are. Property names remain case-sensitive, which is a separate concern. Fixes #65838. See #64974. Co-Authored-By: Claude Opus 5 --- src/wp-includes/kses.php | 19 +++++---- tests/phpunit/tests/kses.php | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 37d457a3e18a2..db9db0bc33014 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2554,6 +2554,7 @@ function kses_init() { * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`. * @since 6.9.0 Added support for `white-space`. * @since 7.1.0 Extended gradient support to allow any single-level nested function. + * CSS function names are now matched case-insensitively. * * @param string $css A string of CSS rules, decoded from an HTML `style` attribute. * @param string $deprecated Not used. @@ -2869,19 +2870,22 @@ function safecss_filter_attr( $css, $deprecated = '' ) { } if ( $is_custom_var ) { - $css_value = trim( $parts[1] ); - $url_attr = str_starts_with( $css_value, 'url(' ); - $gradient_attr = str_contains( $css_value, '-gradient(' ); + $css_value = trim( $parts[1] ); + + // CSS function names are ASCII case-insensitive. + $lowercase_css_value = strtolower( $css_value ); + $url_attr = str_starts_with( $lowercase_css_value, 'url(' ); + $gradient_attr = str_contains( $lowercase_css_value, '-gradient(' ); } } if ( $found && $url_attr ) { // Simplified: matches the sequence `url(*)`. - preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches ); + preg_match_all( '/url\([^)]+\)/i', $parts[1], $url_matches ); foreach ( $url_matches[0] as $url_match ) { // Clean up the URL from each of the matches above. - preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces ); + preg_match( '/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/i', $url_match, $url_pieces ); if ( empty( $url_pieces[2] ) ) { $found = false; @@ -2906,7 +2910,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { * (e.g. rgb(), hsl(), var()). Matching each occurrence, rather than requiring the * whole value to be a single gradient, lets a gradient combine with a url() image. */ - preg_match_all( '/(?:repeating-)?(?:linear|radial|conic)-gradient\((?:[^()]|\([^()]*\))*\)/', $css_test_string, $gradient_matches ); + preg_match_all( '/(?:repeating-)?(?:linear|radial|conic)-gradient\((?:[^()]|\([^()]*\))*\)/i', $css_test_string, $gradient_matches ); foreach ( $gradient_matches[0] as $gradient_match ) { // Remove each `gradient()` bit that was matched above from the CSS. @@ -2918,9 +2922,10 @@ function safecss_filter_attr( $css, $deprecated = '' ) { /* * Allow CSS functions like var(), calc(), etc. by removing them from the test string. * Nested functions and parentheses are also removed, so long as the parentheses are balanced. + * Function names are matched case-insensitively, as they are in CSS. */ $css_test_string = preg_replace( - '/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/', + '/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/i', '', $css_test_string ); diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index 59353a2b7a20c..e0b36c9e859df 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -1003,6 +1003,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_false() { * @ticket 64414 * @ticket 65457 * @ticket 64974 + * @ticket 65838 * * @dataProvider data_safecss_filter_attr * @@ -1545,6 +1546,85 @@ public function data_safecss_filter_attr() { 'css' => 'text-anchor: middle', 'expected' => 'text-anchor: middle', ), + /* + * CSS function names are ASCII case-insensitive. + * + * Note that property names remain case-sensitive, so only the + * function part of each value is written in mixed case here. + */ + array( + 'css' => 'width: CALC(100% - 10px)', + 'expected' => 'width: CALC(100% - 10px)', + ), + array( + 'css' => 'margin-top: Calc(Var(--wp-var1) * 3 + 2em)', + 'expected' => 'margin-top: Calc(Var(--wp-var1) * 3 + 2em)', + ), + array( + 'css' => 'width: CLAMP(MIN(100px, 350px), 50%, MAX(200px, 25%))', + 'expected' => 'width: CLAMP(MIN(100px, 350px), 50%, MAX(200px, 25%))', + ), + array( + 'css' => 'grid-template-columns: REPEAT(4, MINMAX(0, 1fr))', + 'expected' => 'grid-template-columns: REPEAT(4, MINMAX(0, 1fr))', + ), + array( + 'css' => 'background-image: URL("foo.jpg")', + 'expected' => 'background-image: URL("foo.jpg")', + ), + array( + 'css' => 'background: LINEAR-GRADIENT(135deg,RGBA(6,147,227,1) 0%,rgb(155,81,224) 100%)', + 'expected' => 'background: LINEAR-GRADIENT(135deg,RGBA(6,147,227,1) 0%,rgb(155,81,224) 100%)', + ), + array( + 'css' => 'background: Repeating-Radial-Gradient(red, blue)', + 'expected' => 'background: Repeating-Radial-Gradient(red, blue)', + ), + array( + 'css' => '--with-url-value: URL("foo.jpg");--with-gradient: CONIC-GRADIENT(red, blue)', + 'expected' => '--with-url-value: URL("foo.jpg");--with-gradient: CONIC-GRADIENT(red, blue)', + ), + // Case-insensitive matching must not let unsafe or malformed values through. + array( + 'css' => 'background-image: URL("bad://example.com/invalid.gif")', + 'expected' => '', + ), + array( + 'css' => 'background-image: uRl( "JavaScript:alert(1)" )', + 'expected' => '', + ), + array( + 'css' => 'background: LINEAR-GRADIENT(red, URL(javascript:alert(1)))', + 'expected' => '', + ), + array( + 'css' => 'background-image: URL()', + 'expected' => '', + ), + array( + 'css' => 'width: CALC(3em + 10px', + 'expected' => '', + ), + array( + 'css' => 'width: CALC(3em + (10px * 2)', + 'expected' => '', + ), + array( + 'css' => 'width: EXPRESSION(alert(1))', + 'expected' => '', + ), + array( + 'css' => 'aspect-ratio: EXPRESSION( 16 / 9 )', + 'expected' => '', + ), + array( + 'css' => 'background: UNKNOWN-GRADIENT(135deg,rgba(6,147,227,1) 0%)', + 'expected' => '', + ), + array( + 'css' => 'width: CALCMAX(100px + 50%)', + 'expected' => '', + ), ); }