Commit 656aaa1790 for wordpress.org

commit 656aaa17905a5680a14de9a8f9271b8a36c2420e
Author: adamsilverstein <adamsilverstein@git.wordpress.org>
Date:   Sun Oct 18 22:33:49 2026 +0000

    Media: stop adding crossorigin attributes in the editor.

    Fix an issue where editing a post in Chromium 137+ could end in a fatal error and a blank screen when the page loaded any cross-origin resource. `wp_add_crossorigin_attributes()` ran `WP_HTML_Tag_Processor` over the whole document inside an `ob_start()` display handler, where errors cannot be caught. The injected `crossorigin="anonymous"` was not needed in the first place: `Document-Isolation-Policy: isolate-and-credentialless` loads cross-origin scripts, stylesheets, audio and video without credentials instead of blocking them, and forcing a CORS request broke any host that does not send `Access-Control-Allow-Origin`, such as media offloaded to a CDN.

    The header is now sent directly by the new `wp_send_document_isolation_policy_header()`, and the editor page is no longer buffered or rewritten. `wp_start_cross_origin_isolation_output_buffer()` and `wp_add_crossorigin_attributes()` are deprecated; the first only sends the header and the second returns the HTML unchanged. `wp_print_media_templates()` no longer adds the attribute to AUDIO and VIDEO tags. Cross-origin isolation and client-side media processing are unaffected.

    Follow-up to [61844], [62048], [62819].

    Props b0b3k, khokansardar, arkaprabhachowdhury, westonruter, andrewserong, algorithmsunlocks, sabernhardt, swissspidy, adamsilverstein.
    Fixes #65930. See #65673.


    Built from https://develop.svn.wordpress.org/trunk@64489


    git-svn-id: http://core.svn.wordpress.org/trunk@63639 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/deprecated.php b/wp-includes/deprecated.php
index 63aa0d3abe..48aad4751a 100644
--- a/wp-includes/deprecated.php
+++ b/wp-includes/deprecated.php
@@ -6600,3 +6600,41 @@ function _truncate_post_slug( $slug, $length = 200 ) {

 	return wp_truncate_slug( $slug, $length );
 }
+
+/**
+ * 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/wp-includes/media-template.php b/wp-includes/media-template.php
index ee9c6bc21a..a624b81e31 100644
--- a/wp-includes/media-template.php
+++ b/wp-includes/media-template.php
@@ -156,12 +156,6 @@ function wp_underscore_video_template() {
 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. */
 		__( '<a href="%1$s" %2$s>Learn how to describe the purpose of the image%3$s</a>. 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 <script type="text/html"> tags,
-		 * whose content is treated as raw text by the HTML Tag Processor.
-		 * Extract each script block's content, process it separately,
-		 * then reassemble the full output.
-		 */
-		$script_processor = new WP_HTML_Tag_Processor( $html );
-		while ( $script_processor->next_tag( 'SCRIPT' ) ) {
-			if ( 'text/html' !== $script_processor->get_attribute( 'type' ) ) {
-				continue;
-			}
-			/*
-			 * Unlike wp_add_crossorigin_attributes(), this does not check whether
-			 * URLs are actually cross-origin. Media templates use Underscore.js
-			 * template expressions (e.g. {{ data.url }}) as placeholder URLs,
-			 * so actual URLs are not available at parse time.
-			 * The crossorigin attribute is added unconditionally to all relevant
-			 * media tags to ensure cross-origin isolation works regardless of
-			 * the final URL value at render time.
-			 *
-			 * IMG is intentionally excluded, matching wp_add_crossorigin_attributes().
-			 * Under Document-Isolation-Policy: isolate-and-credentialless the browser
-			 * loads cross-origin images in credentialless mode without CORS headers,
-			 * so adding crossorigin="anonymous" would force a CORS request and break
-			 * previews of images served without Access-Control-Allow-Origin headers.
-			 */
-			$template_processor = new WP_HTML_Tag_Processor( $script_processor->get_modifiable_text() );
-			while ( $template_processor->next_tag() ) {
-				if (
-					in_array( $template_processor->get_tag(), array( 'AUDIO', 'VIDEO' ), true )
-					&& ! is_string( $template_processor->get_attribute( 'crossorigin' ) )
-				) {
-					$template_processor->set_attribute( 'crossorigin', 'anonymous' );
-				}
-			}
-			$script_processor->set_modifiable_text( $template_processor->get_updated_html() );
-		}
-
-		echo $script_processor->get_updated_html();
-	}
 }
diff --git a/wp-includes/media.php b/wp-includes/media.php
index 3bb7045611..a1ba0a3213 100644
--- a/wp-includes/media.php
+++ b/wp-includes/media.php
@@ -6648,20 +6648,23 @@ function wp_get_chromium_major_version(): ?int {
  * same-origin iframe access that these editors rely on.
  *
  * @since 7.1.0
+ * @since 7.2.0 Returns whether the header was sent.
+ *
+ * @return bool Whether the Document-Isolation-Policy header was sent.
  */
-function wp_set_up_cross_origin_isolation(): void {
+function wp_set_up_cross_origin_isolation(): bool {
 	if ( ! wp_is_client_side_media_processing_enabled() ) {
-		return;
+		return false;
 	}

 	$screen = get_current_screen();

 	if ( ! $screen ) {
-		return;
+		return false;
 	}

 	if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
-		return;
+		return false;
 	}

 	/*
@@ -6677,7 +6680,7 @@ function wp_set_up_cross_origin_isolation(): void {

 	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
 	if ( 'site-editor.php' === $pagenow && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) {
-		return;
+		return false;
 	}

 	/*
@@ -6686,112 +6689,44 @@ function wp_set_up_cross_origin_isolation(): void {
 	 * which blocks same-origin iframe access that these editors rely on.
 	 */
 	if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
-		return;
+		return false;
 	}

 	// Cross-origin isolation is not needed if users can't upload files anyway.
 	if ( ! current_user_can( 'upload_files' ) ) {
-		return;
+		return false;
 	}

-	wp_start_cross_origin_isolation_output_buffer();
+	return wp_send_document_isolation_policy_header();
 }

 /**
  * Sends the Document-Isolation-Policy header for cross-origin isolation.
  *
- * Uses an output buffer to add crossorigin="anonymous" where needed.
+ * The `isolate-and-credentialless` directive loads cross-origin subresources
+ * without credentials instead of blocking them, so no `crossorigin` attribute
+ * is needed on scripts, styles, images, audio, or video for the page to work.
+ * Forcing `crossorigin="anonymous"` would turn those into CORS requests
+ * and break any resource served without `Access-Control-Allow-Origin`.
  *
- * @since 7.1.0
+ * @since 7.2.0
+ *
+ * @return bool Whether the header was sent. False if the browser does not
+ *              support Document-Isolation-Policy or headers were already sent.
  */
-function wp_start_cross_origin_isolation_output_buffer(): void {
+function wp_send_document_isolation_policy_header(): bool {
+	if ( headers_sent() ) {
+		return false;
+	}
+
 	$chromium_version = wp_get_chromium_major_version();

 	if ( null === $chromium_version || $chromium_version < 137 ) {
-		return;
+		return false;
 	}

-	ob_start(
-		static function ( string $output ): string {
-			header( 'Document-Isolation-Policy: isolate-and-credentialless' );
-
-			return wp_add_crossorigin_attributes( $output );
-		}
-	);
-}
-
-/**
- * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
- *
- * @since 7.1.0
- *
- * @param string $html HTML input.
- * @return string Modified HTML.
- */
-function wp_add_crossorigin_attributes( string $html ): string {
-	$site_url = site_url();
-
-	$processor = new WP_HTML_Tag_Processor( $html );
-
-	// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
-	$cross_origin_tag_attributes = array(
-		'AUDIO'  => array( 'src' ),
-		'LINK'   => array( 'href' ),
-		'SCRIPT' => array( 'src' ),
-		'VIDEO'  => array( 'src', 'poster' ),
-		'SOURCE' => array( 'src' ),
-	);
-
-	while ( $processor->next_tag() ) {
-		$tag = $processor->get_tag();
-
-		if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
-			continue;
-		}
-		$crossorigin = $processor->get_attribute( 'crossorigin' );
-		if ( null !== $crossorigin ) {
-			continue;
-		}
-
-		if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
-			$processor->set_bookmark( 'audio-video-parent' );
-		}
-
-		$processor->set_bookmark( 'resume' );
-
-		$sought = false;
-
-		$is_cross_origin = false;
-
-		foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
-			$url = $processor->get_attribute( $attr );
-			if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
-				$is_cross_origin = true;
-			}
+	header( 'Document-Isolation-Policy: isolate-and-credentialless' );

-			if ( $is_cross_origin ) {
-				break;
-			}
-		}
-
-		if ( $is_cross_origin ) {
-			if ( 'SOURCE' === $tag ) {
-				$sought = $processor->seek( 'audio-video-parent' );
-
-				if ( $sought ) {
-					$processor->set_attribute( 'crossorigin', 'anonymous' );
-				}
-			} else {
-				$processor->set_attribute( 'crossorigin', 'anonymous' );
-			}
-
-			if ( $sought ) {
-				$processor->seek( 'resume' );
-				$processor->release_bookmark( 'audio-video-parent' );
-			}
-		}
-	}
-
-	return $processor->get_updated_html();
+	return true;
 }

diff --git a/wp-includes/version.php b/wp-includes/version.php
index 3f4035e0fd..aa2da05963 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-64488';
+$wp_version = '7.2-alpha-64489';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.