Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2bf9709
attempt to bring 29807.8.diff up to date with trunk
adamsilverstein Feb 26, 2024
11336a3
Merge remote-tracking branch 'upstream/trunk' into pr/6184
azaozz Mar 11, 2024
05dcd69
Fix coding standards spaces and methods visibility
azaozz Mar 11, 2024
499dc82
More CS empty space fixes.
azaozz Mar 11, 2024
7a75eba
Merge branch 'trunk' into ticket/29807
adamsilverstein Aug 15, 2025
69312e7
Apply suggestion from @azaozz
adamsilverstein Aug 15, 2025
3c6defd
string in_array test
adamsilverstein Aug 15, 2025
c88673d
expand test slightly
adamsilverstein Aug 15, 2025
022def5
Cleanup; move multi_uri to parameter
adamsilverstein Aug 15, 2025
321e441
phpcbf
adamsilverstein Aug 15, 2025
c7c63dd
add sizes attribute to test
adamsilverstein Aug 15, 2025
e3c1684
enable sizes
adamsilverstein Aug 15, 2025
3862627
Improve doc block
adamsilverstein Aug 15, 2025
4cb049a
test clean up
adamsilverstein Aug 15, 2025
ca94908
Update tests/phpunit/tests/kses.php
adamsilverstein Aug 17, 2025
49f1ba6
Update src/wp-includes/kses.php
adamsilverstein Aug 17, 2025
2b5706c
remove unused $uris
adamsilverstein Aug 17, 2025
d116c8e
Add additional test cases.
adamsilverstein Aug 17, 2025
fbeed82
Merge branch 'trunk' into ticket/29807
adamsilverstein Aug 27, 2025
f9e002c
phpcbf
adamsilverstein Aug 27, 2025
80521e4
Merge remote-tracking branch 'origin/trunk' into ticket/29807
adamsilverstein Mar 13, 2026
0068aac
Add tests exposing srcset and img attribute bugs
adamsilverstein Mar 13, 2026
f11aeb2
Add decoding and fetchpriority to img allowed attrs
adamsilverstein Mar 13, 2026
cdb84fc
Fix srcset URI sanitization for URLs with commas
adamsilverstein Mar 13, 2026
399aa30
Fix array double arrow alignment in kses tests
adamsilverstein Mar 13, 2026
e5488a6
Merge branch 'trunk' into ticket/29807
adamsilverstein Mar 14, 2026
1f02276
Add kses srcset/picture tests, update @since tag
adamsilverstein Mar 14, 2026
9bb1ebf
Fix array double arrow alignment in new tests
adamsilverstein Mar 14, 2026
2bdc1f9
Merge branch 'trunk' into ticket/29807
adamsilverstein Mar 14, 2026
61881bf
Merge branch 'trunk' into ticket/29807
adamsilverstein Apr 23, 2026
99abbda
KSES: Handle decimal pixel density descriptors in srcset sanitization.
adamsilverstein Apr 23, 2026
8f28de9
Tests: Extend srcset and picture KSES coverage.
adamsilverstein Apr 23, 2026
3917202
Merge remote-tracking branch 'origin/trunk' into ticket/29807
adamsilverstein Jul 10, 2026
fc99930
KSES: Apply review feedback to wp_kses_sanitize_uris().
adamsilverstein Jul 10, 2026
5ea4632
KSES: Sanitize descriptor-less srcset entries individually.
adamsilverstein Jul 10, 2026
215b942
HTML API: Preserve srcset values in set_attribute().
adamsilverstein Jul 10, 2026
e979fc1
Merge branch 'trunk' into ticket/29807
adamsilverstein Jul 15, 2026
a86d336
KSES: Add wp_kses_multi_uri_attributes() as the multi-URI source of t…
adamsilverstein Jul 15, 2026
6d3d20a
KSES: Document accepted srcset limitation for invalid-descriptor commas.
adamsilverstein Jul 15, 2026
bdaca80
Merge branch 'trunk' into ticket/29807
adamsilverstein Jul 15, 2026
3018e2e
Merge branch 'trunk' into ticket/29807
adamsilverstein Jul 15, 2026
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
9 changes: 8 additions & 1 deletion src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4604,7 +4604,14 @@ public function set_attribute( $name, $value ): bool {
*
* @see https://html.spec.whatwg.org/#attributes-3
*/
$escaped_new_value = in_array( $comparable_name, wp_kses_uri_attributes(), true )
/*
* Multi-URI attributes such as srcset contain a comma-separated list
* of URLs with optional width/density descriptors, not a single URL.
* Passing the whole value through esc_url() would encode the
* descriptor spaces and corrupt the list, so those attributes receive
* the standard attribute escaping instead.
*/
$escaped_new_value = ! in_array( $comparable_name, wp_kses_multi_uri_attributes(), true ) && in_array( $comparable_name, wp_kses_uri_attributes(), true )
? esc_url( $value )
: strtr(
$value,
Expand Down
167 changes: 148 additions & 19 deletions src/wp-includes/kses.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,21 @@
),
'i' => array(),
'img' => array(
'alt' => true,
'align' => true,
'border' => true,
'height' => true,
'hspace' => true,
'loading' => true,
'longdesc' => true,
'vspace' => true,
'src' => true,
'usemap' => true,
'width' => true,
'align' => true,
'alt' => true,
'border' => true,
'decoding' => true,
'fetchpriority' => true,
'height' => true,
'hspace' => true,
'loading' => true,
'longdesc' => true,
'sizes' => true,
'src' => true,
'srcset' => true,
'usemap' => true,
'vspace' => true,
'width' => true,
),
'ins' => array(
'datetime' => true,
Expand Down Expand Up @@ -273,6 +277,7 @@
'p' => array(
'align' => true,
),
'picture' => array(),
'pre' => array(
'width' => true,
),
Expand All @@ -298,6 +303,12 @@
'align' => true,
),
'small' => array(),
'source' => array(
'media' => true,
'sizes' => true,
'srcset' => true,
'type' => true,
),
'strike' => array(),
'strong' => array(),
'sub' => array(),
Expand Down Expand Up @@ -981,7 +992,6 @@ function wp_kses( $content, $allowed_html, $allowed_protocols = array() ) {
* @return string Filtered attribute.
*/
function wp_kses_one_attr( $attr, $element ) {
Comment thread
adamsilverstein marked this conversation as resolved.
$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 @@ -1025,10 +1035,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 @@ -1245,6 +1252,7 @@ function wp_kses_uri_attributes() {
'poster',
'profile',
'src',
'srcset',
'usemap',
'xmlns',
);
Expand All @@ -1264,6 +1272,42 @@ function wp_kses_uri_attributes() {
return $uri_attributes;
}

/**
* Returns an array of HTML attribute names whose value contains a list of URLs.
*
* Unlike single-URL attributes such as `href` or `src`, these attributes hold a
* comma-separated list of URLs, each optionally followed by a descriptor, for
* example `srcset="small.jpg 480w, large.jpg 1024w"`. Their values must be split
* into individual candidates so each URL can be sanitized on its own, and must
* not be passed through `esc_url()` as a whole.
*
* An attribute in this list is only sanitized as a URI if it is also present
* in {@see wp_kses_uri_attributes()}.
*
* @since 7.1.0
*
* @return string[] HTML attribute names whose value contains a list of URLs.
*/
function wp_kses_multi_uri_attributes() {
$multi_uri_attributes = array(
'srcset',
);

/**
* Filters the list of attributes whose value contains a list of URLs.
*
* Use this filter to add attributes that, like `srcset`, contain multiple
* comma-separated URLs with optional descriptors. Attributes added here
* must also be added to the `wp_kses_uri_attributes` filter to be
* sanitized as URIs.
*
* @since 7.1.0
*
* @param string[] $multi_uri_attributes HTML attribute names whose value contains a list of URLs.
*/
return apply_filters( 'wp_kses_multi_uri_attributes', $multi_uri_attributes );
}

/**
* Callback for `wp_kses_split()`.
*
Expand Down Expand Up @@ -1624,7 +1668,6 @@ function wp_kses_attr_check( &$name, &$value, &$whole, $vless, $element, $allowe
*/
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();
Expand All @@ -1645,8 +1688,8 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
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 );
if ( is_string( $value ) ) {
$value = wp_kses_sanitize_uris( $name, $value, $allowed_protocols );
}

// Reconstruct and normalize the attribute value.
Expand All @@ -1664,6 +1707,92 @@ function wp_kses_hair( $attr, $allowed_protocols ) {
return $attributes;
}

/**
* 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 7.1.0
*
* @param string $attr_name The attribute name to test.
* @param string $attr_value The attribute value to sanitize.
* @param string[] $allowed_protocols Array of allowed URL protocols.
* @param string[]|null $multi_uri_attrs Optional. Attributes that can contain multiple URIs.
* Default null, meaning the {@see wp_kses_multi_uri_attributes()} list.
* @return string Sanitized attribute value.
*/
function wp_kses_sanitize_uris( string $attr_name, string $attr_value, array $allowed_protocols, ?array $multi_uri_attrs = null ): string {
$attr_name = strtolower( $attr_name );
$uri_attrs = wp_kses_uri_attributes();

if ( null === $multi_uri_attrs ) {
$multi_uri_attrs = wp_kses_multi_uri_attributes();
}

if ( ! in_array( $attr_name, $uri_attrs, true ) ) {
return $attr_value;
}

if ( in_array( $attr_name, $multi_uri_attrs, true ) ) {
/*
* Parse srcset-style attributes into entries so each URL can be sanitized individually.
*
* Srcset entries are: URL [descriptor], URL [descriptor], ...
* Descriptors match patterns like "480w", "2x", or "1.5x", and are optional.
*
* A naive split on all commas breaks URLs that contain commas internally
* (e.g. CDN image resizer URLs like cdn-cgi/image/format=auto,quality=80/...).
*
* Instead, treat a comma as an entry separator only when it cannot be part
* of a URL, which is the case when it:
* - follows a width or pixel density descriptor (e.g. "img.jpg 2x, next.jpg"), or
* - is adjacent to whitespace (URLs in srcset must encode whitespace as %20,
* so a comma next to whitespace always separates two entries).
*
* Known limitation: a comma attached to an *invalid* descriptor (e.g.
* "img.jpg 2q,next.jpg") does not match either rule, so the surrounding
* text is protocol-checked as a single URL. Browsers may parse such an
* invalid srcset into more candidates than KSES does. This asymmetry is
* accepted because srcset candidates are only ever fetched as images;
* a disallowed scheme that rides through this way is never navigated
* to or executed.
*/
$descriptor = '\d+(?:\.\d+)?[wx]';
$delimiter = '\s+' . $descriptor . '\s*,\s*|\s*,\s+|\s+,\s*';
$delim_pattern = '/(' . $delimiter . ')/i';
$delim_anchored = '/^(?:' . $delimiter . ')$/i';
$entry_pattern = '/^(\s*)(.*?)(\s+' . $descriptor . ')?\s*$/i';

$parts = (array) preg_split( $delim_pattern, $attr_value, -1, PREG_SPLIT_DELIM_CAPTURE );
$result = '';

foreach ( $parts as $part ) {
if ( preg_match( $delim_anchored, $part ) ) {
// This is a delimiter between two entries. Append it as-is to preserve spacing.
$result .= $part;
continue;
}

// This is a URL (possibly with a trailing descriptor for the last entry).
if ( preg_match( $entry_pattern, $part, $matches ) ) {
$leading_whitespace = $matches[1];
$url = $matches[2];
$trailing_descriptor = $matches[3] ?? '';
$result .= $leading_whitespace . wp_kses_bad_protocol( $url, $allowed_protocols ) . $trailing_descriptor;
} else {
$result .= wp_kses_bad_protocol( $part, $allowed_protocols );
}
}

return $result;
}

return wp_kses_bad_protocol( $attr_value, $allowed_protocols );
}

/**
* Finds all attributes of an HTML element.
*
Expand Down
76 changes: 76 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,82 @@ public static function data_set_attribute_prevents_xss() {
);
}

/**
* Ensure that setting a srcset attribute preserves the value as-is.
*
* srcset is a URI attribute per wp_kses_uri_attributes(), but unlike single-URL
* attributes it holds a comma-separated list of URLs with optional descriptors.
* Passing the whole value through esc_url() would encode the descriptor spaces
* as %20 and collapse the list into one broken URL.
*
* @ticket 29807
*
* @covers WP_HTML_Tag_Processor::set_attribute
*/
public function test_set_attribute_preserves_srcset_value() {
$srcset = 'small.jpg 480w, medium.jpg 800w, large.jpg 2x';
$processor = new WP_HTML_Tag_Processor( '<img src="small.jpg">' );
$processor->next_tag();
$processor->set_attribute( 'srcset', $srcset );

$this->assertSame(
'<img srcset="small.jpg 480w, medium.jpg 800w, large.jpg 2x" src="small.jpg">',
$processor->get_updated_html(),
'set_attribute() did not preserve the srcset value verbatim'
);
$this->assertSame(
$srcset,
$processor->get_attribute( 'srcset' ),
'get_attribute() did not return the srcset value set via set_attribute()'
);
}

/**
* Ensure that set_attribute() consults wp_kses_multi_uri_attributes() rather
* than hardcoding srcset.
*
* A URI attribute normally passes through esc_url(), which would corrupt a
* srcset-style list by encoding the descriptor spaces. An attribute added to
* both the `wp_kses_uri_attributes` and `wp_kses_multi_uri_attributes`
* filters must skip esc_url() and keep its list value intact.
*
* @ticket 29807
*
* @covers WP_HTML_Tag_Processor::set_attribute
*/
public function test_set_attribute_respects_multi_uri_attributes_filter() {
$srcset_list = 'a.jpg 1x, b.jpg 2x';
$add_custom = static function ( $attrs ) {
$attrs[] = 'data-srcset';
return $attrs;
};

// Registered only as a URI attribute, the value is passed through esc_url() and corrupted.
add_filter( 'wp_kses_uri_attributes', $add_custom );
$processor = new WP_HTML_Tag_Processor( '<img src="a.jpg">' );
$processor->next_tag();
$processor->set_attribute( 'data-srcset', $srcset_list );
$this->assertSame(
'http://a.jpg%201x,%20b.jpg%202x',
$processor->get_attribute( 'data-srcset' ),
'A single-URI attribute should receive esc_url() escaping'
);

// Also registered as a multi-URI attribute, the list value is preserved.
add_filter( 'wp_kses_multi_uri_attributes', $add_custom );
$processor = new WP_HTML_Tag_Processor( '<img src="a.jpg">' );
$processor->next_tag();
$processor->set_attribute( 'data-srcset', $srcset_list );
$this->assertSame(
$srcset_list,
$processor->get_attribute( 'data-srcset' ),
'A multi-URI attribute must not be passed through esc_url()'
);

remove_filter( 'wp_kses_uri_attributes', $add_custom );
remove_filter( 'wp_kses_multi_uri_attributes', $add_custom );
}

/**
* @ticket 56299
*
Expand Down
Loading
Loading