<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[43781] branches/5.0: KSES: Allow `url()` to be used in inline CSS.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/43781">43781</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/43781","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>pento</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2018-10-22 04:03:07 +0000 (Mon, 22 Oct 2018)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>KSES: Allow `url()` to be used in inline CSS.

The cover image block uses the `url()` function in its inline CSS, to show the cover image. KSES didn't allow this, causing the block to not save correctly for Author and Contributor users. As KSES does already check each attribute name against an allowed list, we're able to add an extra check for certain attributes to be able to use the `url()` function, too.

Props peterwilsoncc, azaozz, pento, dd32.
See <a href="https://core.trac.wordpress.org/ticket/45067">#45067</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#branches50srcwpincludesksesphp">branches/5.0/src/wp-includes/kses.php</a></li>
<li><a href="#branches50testsphpunittestsksesphp">branches/5.0/tests/phpunit/tests/kses.php</a></li>
<li><a href="#branches50testsphpunittestsshortcodephp">branches/5.0/tests/phpunit/tests/shortcode.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="branches50srcwpincludesksesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.0/src/wp-includes/kses.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.0/src/wp-includes/kses.php     2018-10-22 03:13:09 UTC (rev 43780)
+++ branches/5.0/src/wp-includes/kses.php       2018-10-22 04:03:07 UTC (rev 43781)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1707,14 +1707,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @return string            Filtered string of CSS rules.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function safecss_filter_attr( $css, $deprecated = '' ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( !empty( $deprecated ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! empty( $deprecated ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $css = wp_kses_no_null($css);
-       $css = str_replace(array("\n","\r","\t"), '', $css);
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $css = wp_kses_no_null( $css );
+       $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( preg_match( '%[\\\\(&=}]|/\*%', $css ) ) // remove any inline css containing \ ( & } = or comments
-               return '';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $allowed_protocols = wp_allowed_protocols();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $css_array = explode( ';', trim( $css ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1724,6 +1724,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 2.8.1
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 4.6.0 Added support for `list-style-type`.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @since 5.0.0 Added support for `background-image`.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $attr List of allowed CSS attributes.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1730,6 +1731,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        $allowed_attr = apply_filters( 'safe_style_css', array(
</span><span class="cx" style="display: block; padding: 0 10px">                'background',
</span><span class="cx" style="display: block; padding: 0 10px">                'background-color',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'background-image',
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                'border',
</span><span class="cx" style="display: block; padding: 0 10px">                'border-width',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1798,25 +1800,83 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'list-style-type',
</span><span class="cx" style="display: block; padding: 0 10px">        ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( empty($allowed_attr) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /*
+        * 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 ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $css;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $css = '';
</span><span class="cx" style="display: block; padding: 0 10px">        foreach ( $css_array as $css_item ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $css_item == '' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( $css_item == '' ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         continue;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $css_item = trim( $css_item );
-               $found = false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         }
+
+               $css_item        = trim( $css_item );
+               $css_test_string = $css_item;
+               $found           = false;
+               $url_attr        = false;
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( strpos( $css_item, ':' ) === false ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $found = true;
</span><span class="cx" style="display: block; padding: 0 10px">                } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $parts = explode( ':', $css_item );
-                       if ( in_array( trim( $parts[0] ), $allowed_attr ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $parts = explode( ':', $css_item, 2 );
+                       $css_selector = trim( $parts[0] );
+
+                       if ( in_array( $css_selector, $allowed_attr, true ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $found = true;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                $url_attr = in_array( $css_selector, $css_url_data_types, true );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $found ) {
-                       if( $css != '' )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               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 != '' ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $css .= ';';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         $css .= $css_item;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span></span></pre></div>
<a id="branches50testsphpunittestsksesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.0/tests/phpunit/tests/kses.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.0/tests/phpunit/tests/kses.php 2018-10-22 03:13:09 UTC (rev 43780)
+++ branches/5.0/tests/phpunit/tests/kses.php   2018-10-22 04:03:07 UTC (rev 43781)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -813,4 +813,151 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        array( 'data**', false ),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * 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 );',
+                               '',
+                       ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="branches50testsphpunittestsshortcodephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: branches/5.0/tests/phpunit/tests/shortcode.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- branches/5.0/tests/phpunit/tests/shortcode.php    2018-10-22 03:13:09 UTC (rev 43780)
+++ branches/5.0/tests/phpunit/tests/shortcode.php      2018-10-22 04:03:07 UTC (rev 43781)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -492,8 +492,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                '<[gallery]>',
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                '<div style="background:url([[gallery]])">',
-                               '<div style="background:url([[gallery]])">',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         '<div style="selector:url([[gallery]])">',
+                               '<div style="selector:url([[gallery]])">',
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><span class="cx" style="display: block; padding: 0 10px">                        array(
</span><span class="cx" style="display: block; padding: 0 10px">                                '[gallery]<div>Hello</div>[/gallery]',
</span></span></pre>
</div>
</div>

</body>
</html>