Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions src/wp-includes/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,10 @@
'longdesc' => true,
'vspace' => true,
'src' => true,
'srcset' => true,
'usemap' => true,
'width' => true,
'sizes' => true,
),
'ins' => array(
'datetime' => true,
Expand Down Expand Up @@ -250,6 +252,7 @@
'p' => array(
'align' => true,
),
'picture' => array(),
'pre' => array(
'width' => true,
),
Expand All @@ -270,6 +273,12 @@
'align' => true,
),
'small' => array(),
'source' => array(
'srcset' => true,
'type' => true,
'media' => true,
'sizes' => true,
),
'strike' => array(),
'strong' => array(),
'sub' => array(),
Expand Down Expand Up @@ -768,7 +777,6 @@ function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) {
* @return string Filtered attribute.
*/
function wp_kses_one_attr( $attr, $element ) {
$uris = wp_kses_uri_attributes();
$allowed_html = wp_kses_allowed_html( 'post' );
$allowed_protocols = wp_allowed_protocols();
$attr = wp_kses_no_null( $attr, array( 'slash_zero' => 'keep' ) );
Expand Down Expand Up @@ -812,10 +820,7 @@ function wp_kses_one_attr( $attr, $element ) {
// Sanitize quotes, angle braces, and entities.
$value = esc_attr( $value );

// Sanitize URI values.
if ( in_array( strtolower( $name ), $uris, true ) ) {
$value = wp_kses_bad_protocol( $value, $allowed_protocols );
}
$value = wp_kses_sanitize_uris( $name, $value, $allowed_protocols );

$attr = "$name=$quote$value$quote";
$vless = 'n';
Expand Down Expand Up @@ -1034,6 +1039,7 @@ function wp_kses_uri_attributes() {
'src',
'usemap',
'xmlns',
'srcset',
);

/**
Expand Down Expand Up @@ -1394,7 +1400,6 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
$attrarr = array();
$mode = 0;
$attrname = '';
$uris = wp_kses_uri_attributes();

// Loop through the whole attribute list.

Expand Down Expand Up @@ -1442,9 +1447,9 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
if ( preg_match( '%^"([^"]*)"(\s+|/?$)%', $attr, $match ) ) {
// "value"
$thisval = $match[1];
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
}

// Sanitize URI values.
$thisval = wp_kses_sanitize_uris( $attrname, $thisval, $allowed_protocols );

if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
Expand All @@ -1464,9 +1469,8 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
if ( preg_match( "%^'([^']*)'(\s+|/?$)%", $attr, $match ) ) {
// 'value'
$thisval = $match[1];
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
}
// Sanitize URI values.
$thisval = wp_kses_sanitize_uris( $attrname, $thisval, $allowed_protocols );

if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
Expand All @@ -1486,9 +1490,8 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
if ( preg_match( "%^([^\s\"']+)(\s+|/?$)%", $attr, $match ) ) {
// value
$thisval = $match[1];
if ( in_array( strtolower( $attrname ), $uris, true ) ) {
$thisval = wp_kses_bad_protocol( $thisval, $allowed_protocols );
}
// Sanitize URI values.
$thisval = wp_kses_sanitize_uris( $attrname, $thisval, $allowed_protocols );

if ( false === array_key_exists( $attrname, $attrarr ) ) {
$attrarr[ $attrname ] = array(
Expand Down Expand Up @@ -1530,6 +1533,42 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
return $attrarr;
}

/**
* Sanitizes URI values in HTML attributes.
*
* This function centralizes logic for cleaning attribute values that are expected to contain URLs.
* It checks if the attribute name is one that should contain a URI (e.g., 'href', 'src', 'srcset').
* For attributes that can contain multiple URIs (such as 'srcset'), it splits the value and sanitizes each URI individually.
* All URI values are passed through {@see wp_kses_bad_protocol()} to remove disallowed protocols (e.g., 'javascript:').
*
* @since 6.9.0
*
* @param string $attrname The attribute name to test.
* @param string $attrvalue The attribute value to sanitize.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @param string[] $multi_uri Optional. Attributes that can contain multiple URIs. Default is array( 'srcset' ).
* @return string Sanitized attribute value.
*/
function wp_kses_sanitize_uris( $attrname, $attrvalue, $allowed_protocols, $multi_uri = array( 'srcset' ) ) {
$uris = wp_kses_uri_attributes();

if ( ! in_array( strtolower( $attrname ), $uris, true ) ) {
return $attrvalue;
} else {
if ( in_array( strtolower( $attrname ), $multi_uri, true ) ) {
$thesevals = preg_split( '/\s*,\s*/', $attrvalue );
} else {
$thesevals = array( $attrvalue );
}
}

foreach ( (array) $thesevals as $key => $val ) {
$thesevals[ $key ] = wp_kses_bad_protocol( $val, $allowed_protocols );
}

return implode( ', ', $thesevals );
}

/**
* Finds all attributes of an HTML element.
*
Expand Down
96 changes: 96 additions & 0 deletions tests/phpunit/tests/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -2377,4 +2377,100 @@ public function data_allowed_attributes_in_descriptions() {
),
);
}

/**
* Test that wp_filter_post_kses() filters img tags correctly and allows the srcset element.
*
* @ticket 29807
*/
public function test_wp_filter_post_kses_img() {
global $allowedposttags;

$attributes = array(
'class' => 'classname',
'id' => 'idattr',
'style' => 'color: red;',
'alt' => 'alt',
'src' => '/test.png',
'srcset' => '/test.png 1x, /test-2x.png 2x, /test-3x.png',
'width' => '100',
'height' => '100',
'usemap' => '#hash',
'vspace' => '20',
'hspace' => '20',
'longdesc' => 'this is the longdesc',
'align' => 'middle',
'border' => '5',
'sizes' => '(max-width: 600px) 100vw, 50vw',
);

foreach ( $attributes as $name => $value ) {
if ( $name === $value ) {
$string = "<img $value />";
$expect_string = '<img ' . trim( $value, ';' ) . ' />';
} else {
$string = "<img $name='$value' />";
$expect_string = "<img $name='" . trim( $value, ';' ) . "' />";
}

$this->assertEquals( $expect_string, wp_kses( $string, $allowedposttags ) );
}
}

/**
* @ticket 29807
*
* @param string $unfiltered Unfiltered srcset value before wp_kses.
* @param string $expected Expected srcset value after wp_kses.
*
* @dataProvider data_wp_kses_srcset
*/
public function test_wp_kses_srcset( $unfiltered, $expected ) {
$unfiltered = "<img src='test.png' srcset='{$unfiltered}' />";
$expected = "<img src='test.png' srcset='{$expected}' />";
$this->assertEquals( $expected, wp_kses_post( $unfiltered ) );
}

public function data_wp_kses_srcset() {
return array(
array(
'/test.png 1x, /test-2x.png 2x',
'/test.png 1x, /test-2x.png 2x',
),
array(
'bad://localhost/test.png 1x, http://localhost/test-2x.png 2x',
'//localhost/test.png 1x, http://localhost/test-2x.png 2x',
),
array(
'http://localhost/test.png 1x, bad://localhost/test-2x.png 2x',
'http://localhost/test.png 1x, //localhost/test-2x.png 2x',
),
array(
'http://localhost/test.png,big 1x, bad://localhost/test.png,medium 2x',
'http://localhost/test.png, big 1x, //localhost/test.png, medium 2x',
),
array(
'path/to/test.png 1x, path/to/test-2x.png 2x',
'path/to/test.png 1x, path/to/test-2x.png 2x',
),
);
}

/**
* @ticket 29807
*/
public function test_wp_filter_post_kses_picture() {
global $allowedposttags;

$html = '<picture><source srcset="pear-mobile.jpeg" media="(max-width: 720px)" type="image/png"><source srcset="pear-tablet.jpeg" media="(max-width: 1280px)" type="image/png"><img src="pear-desktop.jpeg" alt="The pear is juicy."></picture>';
$this->assertEquals( $html, wp_kses( $html, $allowedposttags ) );

$html = '<picture><source srcset="https://wordpress.org/pear-mobile.jpeg" media="(max-width: 720px)" type="image/png"><source srcset="https://wordpress.org/pear-tablet.jpeg 500w, https://wordpress.org/pear-tablet.jpeg" media="(max-width: 1280px)" type="image/png"><img src="pear-desktop.jpeg" alt="The pear is juicy."></picture>';
$this->assertEquals( $html, wp_kses( $html, $allowedposttags ) );

// Test bad protocol in srcset
$original = '<picture><source srcset="bad://pear-mobile.jpeg" media="(max-width: 720px)" type="image/png"><source srcset="pear-tablet.jpeg" media="(max-width: 1280px)" type="image/png"><img src="pear-desktop.jpeg" alt="The pear is juicy."></picture>';
$expected = '<picture><source srcset="//pear-mobile.jpeg" media="(max-width: 720px)" type="image/png"><source srcset="pear-tablet.jpeg" media="(max-width: 1280px)" type="image/png"><img src="pear-desktop.jpeg" alt="The pear is juicy."></picture>';
$this->assertEquals( $expected, wp_kses( $original, $allowedposttags ) );
}
}
Loading