From 984d0fbdcfe89bb11b1faebfff792dadcca147ec Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 8 Sep 2026 09:22:18 -0700 Subject: [PATCH] Media: Stop injecting crossorigin attributes under Document-Isolation-Policy. `isolate-and-credentialless` loads cross-origin subresources without credentials instead of blocking them, so scripts, styles, images, audio, and video from other origins work without a `crossorigin` attribute. Forcing `crossorigin="anonymous"` turns each load into a CORS request, which fails for any host that does not send `Access-Control-Allow-Origin`, such as media offloaded to a CDN. The injection dates from the `require-corp` era of the Gutenberg experiment, where it was needed for anything cross-origin to load. It was carried over unchanged when the editor switched to DIP, and has since caused two regressions (IMG previews, #65673) and a white screen from the tag processor running inside an output buffer display handler (#65930). Send the header directly, deprecate the output buffer helper and the attribute injector, and drop the matching injection from the media templates. Props khokansardar, b0b3k, ianmjones, westonruter, andrewserong. Fixes #65930. See #65673, #64766. --- src/wp-includes/deprecated.php | 38 +++ src/wp-includes/media-template.php | 50 ---- src/wp-includes/media.php | 116 ++------ .../tests/media/wpCrossOriginIsolation.php | 268 ++++-------------- 4 files changed, 111 insertions(+), 361 deletions(-) diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 3b78d1610fdad..e27733a04efab 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -6532,3 +6532,41 @@ function wp_sanitize_script_attributes( $attributes ) { } return $attributes_string; } + +/** + * Starts an output buffer that sends the Document-Isolation-Policy header + * and adds crossorigin="anonymous" to cross-origin resources. + * + * The attribute injection was dropped because `isolate-and-credentialless` + * loads cross-origin resources without it, and forcing CORS mode broke + * resources served without `Access-Control-Allow-Origin`. Only the header + * is sent now. + * + * @since 7.1.0 + * @deprecated 7.2.0 Use wp_send_document_isolation_policy_header() instead. + * @see wp_send_document_isolation_policy_header() + */ +function wp_start_cross_origin_isolation_output_buffer(): void { + _deprecated_function( __FUNCTION__, '7.2.0', 'wp_send_document_isolation_policy_header()' ); + + wp_send_document_isolation_policy_header(); +} + +/** + * Adds crossorigin="anonymous" to relevant tags in the given HTML string. + * + * No longer modifies the HTML. Under `Document-Isolation-Policy: + * isolate-and-credentialless` cross-origin resources load without the + * attribute, and adding it broke resources served without CORS headers. + * + * @since 7.1.0 + * @deprecated 7.2.0 + * + * @param string $html HTML input. + * @return string The unmodified HTML. + */ +function wp_add_crossorigin_attributes( string $html ): string { + _deprecated_function( __FUNCTION__, '7.2.0' ); + + return $html; +} diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 460cf3b3020e5..49332d326c5ce 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -156,12 +156,6 @@ class="wp-video-shortcode {{ classes.join( ' ' ) }}" function wp_print_media_templates() { $class = 'media-modal wp-core-ui'; - $is_cross_origin_isolation_enabled = wp_is_client_side_media_processing_enabled(); - - if ( $is_cross_origin_isolation_enabled ) { - ob_start(); - } - $alt_text_description = sprintf( /* translators: 1: Link to tutorial, 2: Additional link attributes, 3: Accessibility text. */ __( 'Learn how to describe the purpose of the image%3$s. Leave empty if the image is purely decorative.' ), @@ -1596,48 +1590,4 @@ function wp_print_media_templates() { * @since 3.5.0 */ do_action( 'print_media_templates' ); - - if ( $is_cross_origin_isolation_enabled ) { - $html = (string) ob_get_clean(); - - /* - * The media templates are inside ', @@ -413,145 +373,18 @@ public function data_elements_that_should_get_crossorigin() { } /** - * Verifies that certain elements do not get crossorigin="anonymous" added. - * - * Images are excluded because under Document-Isolation-Policy: - * isolate-and-credentialless, the browser handles cross-origin images - * in credentialless mode without needing explicit CORS headers. - * - * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * - * @dataProvider data_elements_that_should_not_get_crossorigin - * - * @param string $html HTML input to process. - */ - public function test_output_buffer_does_not_add_crossorigin( $html ) { - $_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'; - - ob_start(); - - wp_start_cross_origin_isolation_output_buffer(); - echo $html; - - ob_end_flush(); - $output = ob_get_clean(); - - $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output ); - } - - /** - * Data provider for elements that should not receive crossorigin="anonymous". - * - * @return array[] - */ - public function data_elements_that_should_not_get_crossorigin() { - return array( - 'cross-origin img' => array( - '', - ), - 'cross-origin img with srcset' => array( - '', - ), - 'link with cross-origin imagesrcset only' => array( - '', - ), - 'relative URL script' => array( - '', - ), - ); - } - - /** - * Same-origin URLs should not get crossorigin="anonymous". - * - * Uses site_url() at runtime since the test domain varies by CI config. - * - * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_output_buffer_does_not_add_crossorigin_to_same_origin() { - $_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'; - - ob_start(); - - wp_start_cross_origin_isolation_output_buffer(); - echo ''; - - ob_end_flush(); - $output = ob_get_clean(); - - $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output ); - } - - /** - * Elements that already have a crossorigin attribute should not be modified. - * - * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_output_buffer_does_not_override_existing_crossorigin() { - $_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'; - - ob_start(); - - wp_start_cross_origin_isolation_output_buffer(); - echo ''; - - ob_end_flush(); - $output = ob_get_clean(); - - $this->assertStringContainsString( 'crossorigin="use-credentials"', $output, 'Existing crossorigin attribute should not be overridden.' ); - $this->assertStringNotContainsString( 'crossorigin="anonymous"', $output ); - } - - /** - * Multiple tags in the same output should each be handled correctly. - * - * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function test_output_buffer_handles_mixed_tags() { - $_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'; - - ob_start(); - - wp_start_cross_origin_isolation_output_buffer(); - echo ''; - echo ''; - echo ''; - - ob_end_flush(); - $output = ob_get_clean(); - - // IMG should NOT have crossorigin. - $this->assertStringContainsString( '', $output, 'IMG should not be modified.' ); - - // Script and audio should have crossorigin. - $this->assertSame( 2, substr_count( $output, 'crossorigin="anonymous"' ), 'Script and audio should both get crossorigin, but not img.' ); - } - - /** - * IMG tags in the media manager templates must not receive - * crossorigin="anonymous", matching wp_add_crossorigin_attributes(). + * The media manager templates must not carry crossorigin="anonymous". * - * Adding the attribute forces a CORS request that breaks previews of - * images served without Access-Control-Allow-Origin headers, such as - * media offloaded to a CDN. + * Adding the attribute forces a CORS request that breaks previews and + * playback of media served without Access-Control-Allow-Origin headers, + * such as media offloaded to a CDN. * * @ticket 65673 + * @ticket 65930 * * @covers ::wp_print_media_templates */ - public function test_print_media_templates_does_not_add_crossorigin_to_img() { + public function test_print_media_templates_does_not_add_crossorigin() { require_once ABSPATH . WPINC . '/media-template.php'; add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); @@ -560,8 +393,7 @@ public function test_print_media_templates_does_not_add_crossorigin_to_img() { wp_print_media_templates(); $output = ob_get_clean(); - $this->assertMatchesRegularExpression( '/assertDoesNotMatchRegularExpression( '/]*\bcrossorigin\b/i', $output, 'IMG tags in the media templates must not receive a crossorigin attribute.' ); - $this->assertMatchesRegularExpression( '/<(?:audio|video)\b[^>]*crossorigin="anonymous"/i', $output, 'AUDIO and VIDEO tags in the media templates should still receive crossorigin="anonymous".' ); + $this->assertMatchesRegularExpression( '/<(?:img|audio|video)\b/i', $output, 'Expected the media templates to contain media tags.' ); + $this->assertDoesNotMatchRegularExpression( '/<(?:img|audio|video)\b[^>]*\bcrossorigin\b/i', $output, 'Media tags in the media templates must not receive a crossorigin attribute.' ); } }