@@ -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}
0 commit comments