[wp-trac] [WordPress Trac] #65270: wp_kses() corrupts valid CSS background-image: url(...) declarations into style=")" 7.0-RC4
WordPress Trac
noreply at wordpress.org
Tue May 19 14:31:52 UTC 2026
#65270: wp_kses() corrupts valid CSS background-image: url(...) declarations into
style=")" 7.0-RC4
--------------------------+------------------------------
Reporter: nextendweb | Owner: (none)
Type: defect (bug) | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Formatting | Version: trunk
Severity: major | Resolution:
Keywords: | Focuses:
--------------------------+------------------------------
Comment (by nextendweb):
**Another example with & which also breaks**
{{{#!php
<?php
echo wp_kses('<div style="background-image:
url(https://localhost/image.jpg?a=1&b=2);"></div>', [
'div' => [
'style' => ['background-image']
]
]);
}}}
**Related Source Code**
**`wp_kses_hair()`**
The issue appears to begin in `wp_kses_hair()` when the parsed attribute
value is reconstructed and syntax characters are entity-encoded:
{{{
function wp_kses_hair( $attr, $allowed_protocols ) {
$attributes = array();
$uris = wp_kses_uri_attributes();
$processor = new WP_HTML_Tag_Processor( "<wp {$attr}>" );
$processor->next_token();
$attribute_names = $processor->get_attribute_names_with_prefix( '' );
if ( null === $attribute_names || 0 === count( $attribute_names ) ) {
return $attributes;
}
$syntax_characters = array(
'&' => '&',
'<' => '<',
'>' => '>',
"'" => ''',
'"' => '"',
);
foreach ( $attribute_names as $name ) {
$value = $processor->get_attribute( $name );
$is_bool = true === $value;
if ( is_string( $value ) && in_array( $name, $uris, true ) ) {
$value = wp_kses_bad_protocol( $value, $allowed_protocols
);
}
// Reconstruct and normalize the attribute value.
$recoded = $is_bool ? '' : strtr( $value, $syntax_characters );
$whole = $is_bool ? $name : "{$name}=\"{$recoded}\"";
$attributes[ $name ] = array(
'name' => $name,
'value' => $recoded,
'whole' => $whole,
'vless' => $is_bool ? 'y' : 'n',
);
}
return $attributes;
}
}}}
For this input:
{{{
<div style="background-image: url('https://localhost/image.jpg');"></div>
}}}
`wp_kses_hair()` returns the style value as:
{{{
background-image: url('https://localhost/image.jpg');
}}}
`wp_kses_attr_check()`
That encoded value is then passed into `safecss_filter_attr()`:
{{{
if ( 'style' === $name_low ) {
$new_value = safecss_filter_attr( $value );
}
}}}
Debug output:
{{{
string(63) "background-image:
url('https://localhost/image.jpg');"
string(1) ")"
}}}
`safecss_filter_attr()`
The malformed result appears to happen when `safecss_filter_attr()` splits
declarations with `explode( ';', ... )`:
{{{
function safecss_filter_attr( $css, $deprecated = '' ) {
if ( ! empty( $deprecated ) ) {
_deprecated_argument( **FUNCTION**, '2.8.1' ); // Never implemented.
}
```
$css = wp_kses_no_null( $css );
$css = str_replace( array( "\n", "\r", "\t" ), '', $css );
$allowed_protocols = wp_allowed_protocols();
$css_array = explode( ';', trim( $css ) );
```
}}}
Because `'` contains a semicolon, the CSS is split incorrectly:
{{{
background-image: url('https://localhost/image.jpg');
}}}
becomes roughly:
{{{
background-image: url(&apos
[https://localhost/image.jpg&apos](https://localhost/image.jpg&apos)
)
}}}
The last fragment survives sanitization and produces:
{{{
<div style=")"></div>
}}}
**Possible Solutions**
**Solution 1**: Preserve raw quote characters in `wp_kses_hair()`
Avoid storing the entity-encoded value in the internal `value` field
returned by `wp_kses_hair()`.
Potential adjustment:
{{{
$attributes[ $name ] = array(
'name' => $name,
'value' => $is_bool ? '' : $value,
'whole' => $whole,
'vless' => $is_bool ? 'y' : 'n',
);
}}}
This keeps `value` as:
{{{
background-image: url('https://localhost/image.jpg');
}}}
while `whole` can remain safely escaped for HTML reconstruction.
Pros:
* Keeps CSS sanitization operating on raw CSS values.
* Avoids mixing HTML entity encoding with CSS parsing.
* Fixes the WordPress 7.0 regression for valid single-quoted `url(...)`
values.
Cons:
* Does not fix existing failures involving already entity-encoded CSS
values such as `"`.
**Solution 2**: Decode HTML entities before CSS declaration parsing
Decode HTML entities in `safecss_filter_attr()` before splitting CSS
declarations:
{{{
$css = html_entity_decode( $css, ENT_QUOTES | ENT_HTML5,get_option(
'blog_charset' ) );
}}}
before:
{{{
$css_array = explode( ';', trim( $css ) );
}}}
This converts:
{{{
url('https://localhost/image.jpg')
}}}
back into:
{{{
url('https://localhost/image.jpg')
}}}
before `explode( ';', ... )` runs.
Pros:
* Fixes the WordPress 7.0 regression.
* Also fixes pre-existing failures involving entity-encoded CSS values
such as `"`.
* Minimal patch with low implementation complexity.
Cons:
* Changes parsing input semantics globally within `safecss_filter_attr()`.
* Decodes all HTML entities before CSS parsing, which could theoretically
affect obscure edge cases.
**Solution 3**: Decode HTML entities before CSS declaration parsing and
then enconde again
{{{#!php
<?php
function safecss_filter_attr( $css, $deprecated = '' ) {
...
$css = strtr( $css, array(
'&' => '&',
'<' => '<',
'>' => '>',
''' => "'",
'"' => '',
) );
$css_array = explode( ';', trim( $css ) );
...
return strtr( $css, array(
'&' => '&',
'<' => '<',
'>' => '>',
"'" => ''',
'"' => '"',
) );
}
}}}
--
Ticket URL: <https://core.trac.wordpress.org/ticket/65270#comment:1>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list