Skip to content

Commit 5f16124

Browse files
Media: Remove IMG from crossorigin attribute injection.
Under `Document-Isolation-Policy: isolate-and-credentialless`, the browser's credentialless mode already handles cross-origin image loading without requiring CORS headers. Explicitly adding `crossorigin="anonymous"` to `<img>` elements overrides this behavior and forces a CORS preflight request, breaking images from servers that don't include `Access-Control-Allow-Origin` in their response headers. This also removes the related `imagesrcset` handling from `LINK` elements, which had the same issue for `<link>` preload tags for images. See related Gutenberg issue: WordPress/gutenberg#76476. Follow-up to [61844], [61846]. Props adamsilverstein, swissspidy. Fixes #64886. git-svn-id: https://develop.svn.wordpress.org/trunk@62048 602fd350-edb4-49c9-b593-d223f7449a82
1 parent dab1852 commit 5f16124

File tree

2 files changed

+160
-15
lines changed

2 files changed

+160
-15
lines changed

src/wp-includes/media.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6558,14 +6558,7 @@ function wp_add_crossorigin_attributes( string $html ): string {
65586558
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
65596559
$cross_origin_tag_attributes = array(
65606560
'AUDIO' => array( 'src' => false ),
6561-
'IMG' => array(
6562-
'src' => false,
6563-
'srcset' => true,
6564-
),
6565-
'LINK' => array(
6566-
'href' => false,
6567-
'imagesrcset' => true,
6568-
),
6561+
'LINK' => array( 'href' => false ),
65696562
'SCRIPT' => array( 'src' => false ),
65706563
'VIDEO' => array(
65716564
'src' => false,

tests/phpunit/tests/media/wpCrossOriginIsolation.php

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,28 +186,180 @@ public function test_client_side_processing_enabled_on_localhost() {
186186
}
187187

188188
/**
189-
* This test must run in a separate process because the output buffer
190-
* callback sends HTTP headers via header(), which would fail in the
191-
* main PHPUnit process where output has already started.
189+
* Verifies that cross-origin elements get crossorigin="anonymous" added.
192190
*
193191
* @ticket 64766
194192
*
195193
* @runInSeparateProcess
196194
* @preserveGlobalState disabled
195+
*
196+
* @dataProvider data_elements_that_should_get_crossorigin
197+
*
198+
* @param string $html HTML input to process.
197199
*/
198-
public function test_output_buffer_adds_crossorigin_attributes() {
200+
public function test_output_buffer_adds_crossorigin( $html ) {
199201
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
200202

201-
// Start an outer buffer to capture the callback-processed output.
202203
ob_start();
203204

204205
wp_start_cross_origin_isolation_output_buffer();
205-
echo '<img src="https://external.example.com/image.jpg" />';
206+
echo $html;
206207

207-
// Flush the inner buffer to trigger the callback, sending processed output to the outer buffer.
208208
ob_end_flush();
209209
$output = ob_get_clean();
210210

211211
$this->assertStringContainsString( 'crossorigin="anonymous"', $output );
212212
}
213+
214+
/**
215+
* Data provider for elements that should receive crossorigin="anonymous".
216+
*
217+
* @return array[]
218+
*/
219+
public function data_elements_that_should_get_crossorigin() {
220+
return array(
221+
'cross-origin script' => array(
222+
'<script src="https://external.example.com/script.js"></script>',
223+
),
224+
'cross-origin audio' => array(
225+
'<audio src="https://external.example.com/audio.mp3"></audio>',
226+
),
227+
'cross-origin video' => array(
228+
'<video src="https://external.example.com/video.mp4"></video>',
229+
),
230+
'cross-origin link stylesheet' => array(
231+
'<link rel="stylesheet" href="https://external.example.com/style.css" />',
232+
),
233+
'cross-origin source inside video' => array(
234+
'<video><source src="https://external.example.com/video.mp4" type="video/mp4" /></video>',
235+
),
236+
);
237+
}
238+
239+
/**
240+
* Verifies that certain elements do not get crossorigin="anonymous" added.
241+
*
242+
* Images are excluded because under Document-Isolation-Policy:
243+
* isolate-and-credentialless, the browser handles cross-origin images
244+
* in credentialless mode without needing explicit CORS headers.
245+
*
246+
* @ticket 64766
247+
*
248+
* @runInSeparateProcess
249+
* @preserveGlobalState disabled
250+
*
251+
* @dataProvider data_elements_that_should_not_get_crossorigin
252+
*
253+
* @param string $html HTML input to process.
254+
*/
255+
public function test_output_buffer_does_not_add_crossorigin( $html ) {
256+
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
257+
258+
ob_start();
259+
260+
wp_start_cross_origin_isolation_output_buffer();
261+
echo $html;
262+
263+
ob_end_flush();
264+
$output = ob_get_clean();
265+
266+
$this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
267+
}
268+
269+
/**
270+
* Data provider for elements that should not receive crossorigin="anonymous".
271+
*
272+
* @return array[]
273+
*/
274+
public function data_elements_that_should_not_get_crossorigin() {
275+
return array(
276+
'cross-origin img' => array(
277+
'<img src="https://external.example.com/image.jpg" />',
278+
),
279+
'cross-origin img with srcset' => array(
280+
'<img src="https://external.example.com/image.jpg" srcset="https://external.example.com/image-2x.jpg 2x" />',
281+
),
282+
'link with cross-origin imagesrcset only' => array(
283+
'<link rel="preload" as="image" imagesrcset="https://external.example.com/image.jpg 1x" href="/local-fallback.jpg" />',
284+
),
285+
'relative URL script' => array(
286+
'<script src="/wp-includes/js/wp-embed.min.js"></script>',
287+
),
288+
);
289+
}
290+
291+
/**
292+
* Same-origin URLs should not get crossorigin="anonymous".
293+
*
294+
* Uses site_url() at runtime since the test domain varies by CI config.
295+
*
296+
* @ticket 64766
297+
*
298+
* @runInSeparateProcess
299+
* @preserveGlobalState disabled
300+
*/
301+
public function test_output_buffer_does_not_add_crossorigin_to_same_origin() {
302+
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
303+
304+
ob_start();
305+
306+
wp_start_cross_origin_isolation_output_buffer();
307+
echo '<script src="' . site_url( '/wp-includes/js/wp-embed.min.js' ) . '"></script>';
308+
309+
ob_end_flush();
310+
$output = ob_get_clean();
311+
312+
$this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
313+
}
314+
315+
/**
316+
* Elements that already have a crossorigin attribute should not be modified.
317+
*
318+
* @ticket 64766
319+
*
320+
* @runInSeparateProcess
321+
* @preserveGlobalState disabled
322+
*/
323+
public function test_output_buffer_does_not_override_existing_crossorigin() {
324+
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
325+
326+
ob_start();
327+
328+
wp_start_cross_origin_isolation_output_buffer();
329+
echo '<script src="https://external.example.com/script.js" crossorigin="use-credentials"></script>';
330+
331+
ob_end_flush();
332+
$output = ob_get_clean();
333+
334+
$this->assertStringContainsString( 'crossorigin="use-credentials"', $output, 'Existing crossorigin attribute should not be overridden.' );
335+
$this->assertStringNotContainsString( 'crossorigin="anonymous"', $output );
336+
}
337+
338+
/**
339+
* Multiple tags in the same output should each be handled correctly.
340+
*
341+
* @ticket 64766
342+
*
343+
* @runInSeparateProcess
344+
* @preserveGlobalState disabled
345+
*/
346+
public function test_output_buffer_handles_mixed_tags() {
347+
$_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
348+
349+
ob_start();
350+
351+
wp_start_cross_origin_isolation_output_buffer();
352+
echo '<img src="https://external.example.com/image.jpg" />';
353+
echo '<script src="https://external.example.com/script.js"></script>';
354+
echo '<audio src="https://external.example.com/audio.mp3"></audio>';
355+
356+
ob_end_flush();
357+
$output = ob_get_clean();
358+
359+
// IMG should NOT have crossorigin.
360+
$this->assertStringContainsString( '<img src="https://external.example.com/image.jpg" />', $output, 'IMG should not be modified.' );
361+
362+
// Script and audio should have crossorigin.
363+
$this->assertSame( 2, substr_count( $output, 'crossorigin="anonymous"' ), 'Script and audio should both get crossorigin, but not img.' );
364+
}
213365
}

0 commit comments

Comments
 (0)