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.