diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 8cc521346a..964ef7fd6e 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1985,9 +1985,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { $css = wp_kses_no_null( $css ); $css = str_replace( array( "\n", "\r", "\t" ), '', $css ); - if ( preg_match( '%[\\\\(&=}]|/\*%', $css ) ) { // remove any inline css containing \ ( & } = or comments - return ''; - } + $allowed_protocols = wp_allowed_protocols(); $css_array = explode( ';', trim( $css ) ); @@ -1998,6 +1996,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`. * @since 4.6.0 Added support for `list-style-type`. * @since 5.0.0 Added support for `text-transform`. + * @since 5.0.0 Added support for `background-image`. * * @param string[] $attr Array of allowed CSS attributes. */ @@ -2006,6 +2005,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { array( 'background', 'background-color', + 'background-image', 'border', 'border-width', @@ -2076,6 +2076,24 @@ function safecss_filter_attr( $css, $deprecated = '' ) { ) ); + /* + * CSS attributes that accept URL data types. + * + * This is in accordance to the CSS spec and unrelated to + * the sub-set of supported attributes above. + * + * See: https://developer.mozilla.org/en-US/docs/Web/CSS/url + */ + $css_url_data_types = array( + 'background', + 'background-image', + + 'cursor', + + 'list-style', + 'list-style-image', + ); + if ( empty( $allowed_attr ) ) { return $css; } @@ -2085,20 +2103,55 @@ function safecss_filter_attr( $css, $deprecated = '' ) { if ( $css_item == '' ) { continue; } - $css_item = trim( $css_item ); - $found = false; + + $css_item = trim( $css_item ); + $css_test_string = $css_item; + $found = false; + $url_attr = false; + if ( strpos( $css_item, ':' ) === false ) { $found = true; } else { - $parts = explode( ':', $css_item ); - if ( in_array( trim( $parts[0] ), $allowed_attr ) ) { - $found = true; + $parts = explode( ':', $css_item, 2 ); + $css_selector = trim( $parts[0] ); + + if ( in_array( $css_selector, $allowed_attr, true ) ) { + $found = true; + $url_attr = in_array( $css_selector, $css_url_data_types, true ); } } - if ( $found ) { + + if ( $found && $url_attr ) { + // Simplified: matches the sequence `url(*)`. + preg_match_all( '/url\([^)]+\)/', $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 ); + + if ( empty( $url_pieces[2] ) ) { + $found = false; + break; + } + + $url = trim( $url_pieces[2] ); + + if ( empty( $url ) || $url !== wp_kses_bad_protocol( $url, $allowed_protocols ) ) { + $found = false; + break; + } else { + // Remove the whole `url(*)` bit that was matched above from the CSS. + $css_test_string = str_replace( $url_match, '', $css_test_string ); + } + } + } + + // Remove any CSS containing containing \ ( & } = or comments, except for url() useage checked above. + if ( $found && ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string ) ) { if ( $css != '' ) { $css .= ';'; } + $css .= $css_item; } } diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php index b91fe5d386..ae1ccd342f 100644 --- a/tests/phpunit/tests/kses.php +++ b/tests/phpunit/tests/kses.php @@ -818,10 +818,10 @@ EOF; 'css' => 'margin: 10px 20px;padding: 5px 10px', 'expected' => 'margin: 10px 20px;padding: 5px 10px', ), - // Parenthesis ( isn't supported. + // Parenthesis ( is supported for some attributes. array( 'css' => 'background: green url("foo.jpg") no-repeat fixed center', - 'expected' => '', + 'expected' => 'background: green url("foo.jpg") no-repeat fixed center', ), ); } @@ -920,4 +920,151 @@ EOF; array( 'data**', false ), ); } + + /** + * Test URL sanitization in the style tag. + * + * @dataProvider data_kses_style_attr_with_url + * + * @ticket 45067 + * + * @param $input string The style attribute saved in the editor. + * @param $expected string The sanitized style attribute. + */ + function test_kses_style_attr_with_url( $input, $expected ) { + $actual = safecss_filter_attr( $input ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider testing style attribute sanitization. + * + * @return array Nested array of input, expected pairs. + */ + function data_kses_style_attr_with_url() { + return array( + /* + * Valid use cases. + */ + + // Double quotes. + array( + 'background-image: url( "http://example.com/valid.gif" );', + 'background-image: url( "http://example.com/valid.gif" )', + ), + + // Single quotes. + array( + "background-image: url( 'http://example.com/valid.gif' );", + "background-image: url( 'http://example.com/valid.gif' )", + ), + + // No quotes. + array( + 'background-image: url( http://example.com/valid.gif );', + 'background-image: url( http://example.com/valid.gif )', + ), + + // Single quotes, extra spaces. + array( + "background-image: url( ' http://example.com/valid.gif ' );", + "background-image: url( ' http://example.com/valid.gif ' )", + ), + + // Line breaks, single quotes. + array( + "background-image: url(\n'http://example.com/valid.gif' );", + "background-image: url('http://example.com/valid.gif' )", + ), + + // Tabs not spaces, single quotes. + array( + "background-image: url(\t'http://example.com/valid.gif'\t\t);", + "background-image: url('http://example.com/valid.gif')", + ), + + // Single quotes, absolute path. + array( + "background: url('/valid.gif');", + "background: url('/valid.gif')", + ), + + // Single quotes, relative path. + array( + "background: url('../wp-content/uploads/2018/10/valid.gif');", + "background: url('../wp-content/uploads/2018/10/valid.gif')", + ), + + // Error check: valid property not containing a URL. + array( + 'background: red', + 'background: red', + ), + + /* + * Invalid use cases. + */ + + // Attribute doesn't support URL properties. + array( + 'color: url( "http://example.com/invalid.gif" );', + '', + ), + + // Mismatched quotes. + array( + 'background-image: url( "http://example.com/valid.gif\' );', + '', + ), + + // Bad protocol, double quotes. + array( + 'background-image: url( "bad://example.com/invalid.gif" );', + '', + ), + + // Bad protocol, single quotes. + array( + "background-image: url( 'bad://example.com/invalid.gif' );", + '', + ), + + // Bad protocol, single quotes. + array( + "background-image: url( 'bad://example.com/invalid.gif' );", + '', + ), + + // Bad protocol, single quotes, strange spacing. + array( + "background-image: url( ' \tbad://example.com/invalid.gif ' );", + '', + ), + + // Bad protocol, no quotes. + array( + 'background-image: url( bad://example.com/invalid.gif );', + '', + ), + + // No URL inside url(). + array( + 'background-image: url();', + '', + ), + + // Malformed, no closing `)`. + array( + 'background-image: url( "http://example.com" ;', + '', + ), + + // Malformed, no closing `"`. + array( + 'background-image: url( "http://example.com );', + '', + ), + ); + } } diff --git a/tests/phpunit/tests/shortcode.php b/tests/phpunit/tests/shortcode.php index 790325dd26..050baa5c0a 100644 --- a/tests/phpunit/tests/shortcode.php +++ b/tests/phpunit/tests/shortcode.php @@ -561,8 +561,8 @@ EOF; '<[gallery]>', ), array( - '
', - '
', + '
', + '
', ), array( '[gallery]
Hello
[/gallery]',