Skip to content

Commit 0068aac

Browse files
Add tests exposing srcset and img attribute bugs
Tests currently fail, documenting three bugs: - CDN resizer URLs with commas in paths are incorrectly split by wp_kses_sanitize_uris() naive comma splitting - Original spacing around commas is not preserved - decoding and fetchpriority attrs stripped from img tags Also adds passing coverage tests for wp_kses_uri_attributes, wp_kses_one_attr srcset handling, source/picture element edge cases, and the URI attributes filter hook.
1 parent 80521e4 commit 0068aac

2 files changed

Lines changed: 219 additions & 2 deletions

File tree

tests/phpunit/tests/kses.php

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,7 +2465,7 @@ public function test_wp_filter_post_kses_img() {
24652465
$expect_string = '<img ' . trim( $value, ';' ) . ' />';
24662466
} else {
24672467
$string = "<img $name='$value' />";
2468-
$expect_string = "<img $name='" . trim( $value, ';' ) . "' />";
2468+
$expect_string = '<img ' . $name . '="' . trim( $value, ';' ) . '" />';
24692469
}
24702470

24712471
$this->assertEquals( $expect_string, wp_kses( $string, $allowedposttags ) );
@@ -2482,7 +2482,7 @@ public function test_wp_filter_post_kses_img() {
24822482
*/
24832483
public function test_wp_kses_srcset( $unfiltered, $expected ) {
24842484
$unfiltered = "<img src='test.png' srcset='{$unfiltered}' />";
2485-
$expected = "<img src='test.png' srcset='{$expected}' />";
2485+
$expected = '<img src="test.png" srcset="' . $expected . '" />';
24862486
$this->assertEquals( $expected, wp_kses_post( $unfiltered ) );
24872487
}
24882488

@@ -2668,4 +2668,195 @@ public function test_wp_kses_comprehensive_responsive_images() {
26682668
$this->assertStringContainsString( '<picture>', $result );
26692669
$this->assertStringContainsString( '<source', $result );
26702670
}
2671+
2672+
/**
2673+
* Test that srcset URLs containing commas in the URL path are not broken.
2674+
*
2675+
* CDN image resizers (e.g. Cloudflare) use commas in URL paths like:
2676+
* cdn-cgi/image/format=auto,quality=80,width=412/https://bucket.example/img.jpg
2677+
*
2678+
* The srcset splitting logic must distinguish between commas that separate
2679+
* srcset entries (followed by a URL) and commas within a single URL.
2680+
*
2681+
* @ticket 29807
2682+
* @dataProvider data_wp_kses_srcset_with_commas_in_urls
2683+
*/
2684+
public function test_wp_kses_srcset_with_commas_in_urls( $input, $expected ) {
2685+
$unfiltered = "<img src='test.png' srcset='{$input}' />";
2686+
$expected = '<img src="test.png" srcset="' . $expected . '" />';
2687+
$this->assertSame( $expected, wp_kses_post( $unfiltered ) );
2688+
}
2689+
2690+
public function data_wp_kses_srcset_with_commas_in_urls() {
2691+
return array(
2692+
'CDN resizer URL with commas in path, multiple srcset entries' => array(
2693+
'https://resizer.example/cdn-cgi/image/format=auto,onerror=redirect,quality=80,width=412,height=275,dpr=1,fit=crop,gravity=0.5x0.5/https://bucket.example/wp-content/uploads/2025/08/photo.jpg 412w, https://resizer.example/cdn-cgi/image/format=auto,onerror=redirect,quality=80,width=824,height=550,dpr=1,fit=crop,gravity=0.5x0.5/https://bucket.example/wp-content/uploads/2025/08/photo.jpg 824w',
2694+
'https://resizer.example/cdn-cgi/image/format=auto,onerror=redirect,quality=80,width=412,height=275,dpr=1,fit=crop,gravity=0.5x0.5/https://bucket.example/wp-content/uploads/2025/08/photo.jpg 412w, https://resizer.example/cdn-cgi/image/format=auto,onerror=redirect,quality=80,width=824,height=550,dpr=1,fit=crop,gravity=0.5x0.5/https://bucket.example/wp-content/uploads/2025/08/photo.jpg 824w',
2695+
),
2696+
'single CDN resizer URL with commas, no srcset separator' => array(
2697+
'https://resizer.example/cdn-cgi/image/format=auto,quality=80/https://bucket.example/img.jpg',
2698+
'https://resizer.example/cdn-cgi/image/format=auto,quality=80/https://bucket.example/img.jpg',
2699+
),
2700+
'CDN resizer URL with commas, pixel density descriptor' => array(
2701+
'https://resizer.example/cdn-cgi/image/format=auto,quality=80,width=200/https://bucket.example/img.jpg 1x, https://resizer.example/cdn-cgi/image/format=auto,quality=80,width=400/https://bucket.example/img.jpg 2x',
2702+
'https://resizer.example/cdn-cgi/image/format=auto,quality=80,width=200/https://bucket.example/img.jpg 1x, https://resizer.example/cdn-cgi/image/format=auto,quality=80,width=400/https://bucket.example/img.jpg 2x',
2703+
),
2704+
);
2705+
}
2706+
2707+
/**
2708+
* Test that srcset values preserve their original spacing.
2709+
*
2710+
* wp_kses_sanitize_uris() should not add or remove spaces around commas.
2711+
*
2712+
* @ticket 29807
2713+
* @dataProvider data_wp_kses_srcset_preserves_spacing
2714+
*/
2715+
public function test_wp_kses_srcset_preserves_spacing( $input, $expected ) {
2716+
$allowed_protocols = wp_allowed_protocols();
2717+
$result = wp_kses_sanitize_uris( 'srcset', $input, $allowed_protocols );
2718+
$this->assertSame( $expected, $result );
2719+
}
2720+
2721+
public function data_wp_kses_srcset_preserves_spacing() {
2722+
return array(
2723+
'no space after comma' => array(
2724+
'image1.jpg 1x,image2.jpg 2x',
2725+
'image1.jpg 1x,image2.jpg 2x',
2726+
),
2727+
'single space after comma' => array(
2728+
'image1.jpg 1x, image2.jpg 2x',
2729+
'image1.jpg 1x, image2.jpg 2x',
2730+
),
2731+
'multiple spaces after comma' => array(
2732+
'image1.jpg 1x, image2.jpg 2x',
2733+
'image1.jpg 1x, image2.jpg 2x',
2734+
),
2735+
);
2736+
}
2737+
2738+
/**
2739+
* Test that decoding and fetchpriority attributes are allowed on img tags.
2740+
*
2741+
* These attributes are commonly added by WordPress core for performance
2742+
* optimization and should not be stripped by KSES.
2743+
*
2744+
* @ticket 29807
2745+
*/
2746+
public function test_wp_kses_img_decoding_and_fetchpriority() {
2747+
global $allowedposttags;
2748+
2749+
// Test decoding attribute.
2750+
$html = '<img src="test.jpg" decoding="async" />';
2751+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2752+
2753+
// Test fetchpriority attribute.
2754+
$html = '<img src="test.jpg" fetchpriority="high" />';
2755+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2756+
2757+
// Test full real-world img tag with all responsive attributes.
2758+
$html = '<img src="test.jpg" decoding="async" fetchpriority="high" srcset="small.jpg 1x, large.jpg 2x" sizes="100vw" loading="lazy" />';
2759+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2760+
}
2761+
2762+
/**
2763+
* Test that wp_kses_uri_attributes() includes srcset.
2764+
*
2765+
* @ticket 29807
2766+
* @covers ::wp_kses_uri_attributes
2767+
*/
2768+
public function test_wp_kses_uri_attributes_includes_srcset() {
2769+
$uri_attrs = wp_kses_uri_attributes();
2770+
2771+
$this->assertContains( 'srcset', $uri_attrs, 'srcset should be a URI attribute.' );
2772+
$this->assertContains( 'src', $uri_attrs, 'src should be a URI attribute.' );
2773+
$this->assertContains( 'href', $uri_attrs, 'href should be a URI attribute.' );
2774+
$this->assertContains( 'action', $uri_attrs, 'action should be a URI attribute.' );
2775+
}
2776+
2777+
/**
2778+
* Test wp_kses_one_attr() with srcset attribute.
2779+
*
2780+
* @ticket 29807
2781+
* @covers ::wp_kses_one_attr
2782+
*/
2783+
public function test_wp_kses_one_attr_srcset() {
2784+
// Valid multi-URI srcset passes through.
2785+
$result = wp_kses_one_attr( ' srcset="image1.jpg 1x, image2.jpg 2x"', 'img' );
2786+
$this->assertSame( ' srcset="image1.jpg 1x, image2.jpg 2x"', $result );
2787+
2788+
// Bad protocol in srcset is stripped.
2789+
$result = wp_kses_one_attr( ' srcset="javascript:alert(1) 1x, https://example.com/img.jpg 2x"', 'img' );
2790+
$this->assertStringNotContainsString( 'javascript:', $result );
2791+
$this->assertStringContainsString( 'https://example.com/img.jpg', $result );
2792+
}
2793+
2794+
/**
2795+
* Test source element attribute handling.
2796+
*
2797+
* @ticket 29807
2798+
*/
2799+
public function test_wp_kses_source_element_attributes() {
2800+
global $allowedposttags;
2801+
2802+
// All four allowed attributes together.
2803+
$html = '<source srcset="img.jpg" type="image/webp" media="(min-width: 800px)" sizes="100vw">';
2804+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2805+
2806+
// Disallowed attribute (src) is stripped from source.
2807+
$original = '<source srcset="img.jpg" src="fallback.jpg">';
2808+
$expected = '<source srcset="img.jpg">';
2809+
$this->assertSame( $expected, wp_kses( $original, $allowedposttags ) );
2810+
2811+
// Event handler is stripped.
2812+
$original = '<source srcset="img.jpg" onerror="alert(1)">';
2813+
$expected = '<source srcset="img.jpg">';
2814+
$this->assertSame( $expected, wp_kses( $original, $allowedposttags ) );
2815+
}
2816+
2817+
/**
2818+
* Test picture element edge cases.
2819+
*
2820+
* @ticket 29807
2821+
*/
2822+
public function test_wp_kses_picture_element_edge_cases() {
2823+
global $allowedposttags;
2824+
2825+
// Empty picture element passes through.
2826+
$html = '<picture></picture>';
2827+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2828+
2829+
// Picture without fallback img.
2830+
$html = '<picture><source srcset="img.webp" type="image/webp"></picture>';
2831+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2832+
2833+
// Picture with only img, no source elements.
2834+
$html = '<picture><img src="only-img.jpg" /></picture>';
2835+
$this->assertSame( $html, wp_kses( $html, $allowedposttags ) );
2836+
}
2837+
2838+
/**
2839+
* Test that the wp_kses_uri_attributes filter affects srcset sanitization.
2840+
*
2841+
* @ticket 29807
2842+
*/
2843+
public function test_wp_kses_uri_attributes_filter() {
2844+
$allowed_protocols = wp_allowed_protocols();
2845+
2846+
// Remove srcset from URI attributes via filter.
2847+
$remove_srcset = static function ( $uri_attrs ) {
2848+
return array_diff( $uri_attrs, array( 'srcset' ) );
2849+
};
2850+
add_filter( 'wp_kses_uri_attributes', $remove_srcset );
2851+
2852+
// Bad protocol in srcset should NOT be stripped since srcset is no longer a URI attribute.
2853+
$result = wp_kses_sanitize_uris( 'srcset', 'javascript:alert(1) 1x', $allowed_protocols );
2854+
$this->assertSame( 'javascript:alert(1) 1x', $result );
2855+
2856+
remove_filter( 'wp_kses_uri_attributes', $remove_srcset );
2857+
2858+
// After removing filter, bad protocol should be stripped again.
2859+
$result = wp_kses_sanitize_uris( 'srcset', 'javascript:alert(1) 1x', $allowed_protocols );
2860+
$this->assertStringNotContainsString( 'javascript:', $result );
2861+
}
26712862
}

tests/phpunit/tests/kses/wpKsesHair.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,6 +1086,32 @@ public function data_protocol_filtering() {
10861086
),
10871087
),
10881088
);
1089+
1090+
// @ticket 29807
1091+
yield 'srcset with valid multi-URI and width descriptors' => array(
1092+
'srcset="small.jpg 480w, large.jpg 1024w"',
1093+
array(
1094+
'srcset' => array(
1095+
'name' => 'srcset',
1096+
'value' => 'small.jpg 480w, large.jpg 1024w',
1097+
'whole' => 'srcset="small.jpg 480w, large.jpg 1024w"',
1098+
'vless' => 'n',
1099+
),
1100+
),
1101+
);
1102+
1103+
// @ticket 29807
1104+
yield 'srcset with bad protocol in one of multiple URIs' => array(
1105+
'srcset="javascript:alert(1) 1x, https://example.com/img.jpg 2x"',
1106+
array(
1107+
'srcset' => array(
1108+
'name' => 'srcset',
1109+
'value' => 'alert(1) 1x, https://example.com/img.jpg 2x',
1110+
'whole' => 'srcset="alert(1) 1x, https://example.com/img.jpg 2x"',
1111+
'vless' => 'n',
1112+
),
1113+
),
1114+
);
10891115
}
10901116

10911117
/**

0 commit comments

Comments
 (0)