Make WordPress Core


Ignore:
Timestamp:
10/18/2026 10:32:14 PM (40 hours ago)
Author:
adamsilverstein
Message:

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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/media.php

    r64436 r64489  
    66496649 *
    66506650 * @since 7.1.0
    6651  */
    6652 function wp_set_up_cross_origin_isolation(): void {
     6651 * @since 7.2.0 Returns whether the header was sent.
     6652 *
     6653 * @return bool Whether the Document-Isolation-Policy header was sent.
     6654 */
     6655function wp_set_up_cross_origin_isolation(): bool {
    66536656        if ( ! wp_is_client_side_media_processing_enabled() ) {
    6654                 return;
     6657                return false;
    66556658        }
    66566659
    … …  
    66586661
    66596662        if ( ! $screen ) {
    6660                 return;
     6663                return false;
    66616664        }
    66626665
    66636666        if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) {
    6664                 return;
     6667                return false;
    66656668        }
    66666669
    … …  
    66786681        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
    66796682        if ( 'site-editor.php' === $pagenow && ! wp_is_block_theme() && ( ! isset( $_GET['p'] ) || '/' === $_GET['p'] ) ) {
    6680                 return;
     6683                return false;
    66816684        }
    66826685
    … …  
    66876690         */
    66886691        if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
    6689                 return;
     6692                return false;
    66906693        }
    66916694
    66926695        // Cross-origin isolation is not needed if users can't upload files anyway.
    66936696        if ( ! current_user_can( 'upload_files' ) ) {
    6694                 return;
    6695         }
    6696 
    6697         wp_start_cross_origin_isolation_output_buffer();
     6697                return false;
     6698        }
     6699
     6700        return wp_send_document_isolation_policy_header();
    66986701}
    66996702
    … …  
    67016704 * Sends the Document-Isolation-Policy header for cross-origin isolation.
    67026705 *
    6703  * Uses an output buffer to add crossorigin="anonymous" where needed.
    6704  *
    6705  * @since 7.1.0
    6706  */
    6707 function wp_start_cross_origin_isolation_output_buffer(): void {
     6706 * The `isolate-and-credentialless` directive loads cross-origin subresources
     6707 * without credentials instead of blocking them, so no `crossorigin` attribute
     6708 * is needed on scripts, styles, images, audio, or video for the page to work.
     6709 * Forcing `crossorigin="anonymous"` would turn those into CORS requests
     6710 * and break any resource served without `Access-Control-Allow-Origin`.
     6711 *
     6712 * @since 7.2.0
     6713 *
     6714 * @return bool Whether the header was sent. False if the browser does not
     6715 *              support Document-Isolation-Policy or headers were already sent.
     6716 */
     6717function wp_send_document_isolation_policy_header(): bool {
     6718        if ( headers_sent() ) {
     6719                return false;
     6720        }
     6721
    67086722        $chromium_version = wp_get_chromium_major_version();
    67096723
    67106724        if ( null === $chromium_version || $chromium_version < 137 ) {
    6711                 return;
    6712         }
    6713 
    6714         ob_start(
    6715                 static function ( string $output ): string {
    6716                         header( 'Document-Isolation-Policy: isolate-and-credentialless' );
    6717 
    6718                         return wp_add_crossorigin_attributes( $output );
    6719                 }
    6720         );
    6721 }
    6722 
    6723 /**
    6724  * Adds crossorigin="anonymous" to relevant tags in the given HTML string.
    6725  *
    6726  * @since 7.1.0
    6727  *
    6728  * @param string $html HTML input.
    6729  * @return string Modified HTML.
    6730  */
    6731 function wp_add_crossorigin_attributes( string $html ): string {
    6732         $site_url = site_url();
    6733 
    6734         $processor = new WP_HTML_Tag_Processor( $html );
    6735 
    6736         // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin.
    6737         $cross_origin_tag_attributes = array(
    6738                 'AUDIO'  => array( 'src' ),
    6739                 'LINK'   => array( 'href' ),
    6740                 'SCRIPT' => array( 'src' ),
    6741                 'VIDEO'  => array( 'src', 'poster' ),
    6742                 'SOURCE' => array( 'src' ),
    6743         );
    6744 
    6745         while ( $processor->next_tag() ) {
    6746                 $tag = $processor->get_tag();
    6747 
    6748                 if ( ! isset( $cross_origin_tag_attributes[ $tag ] ) ) {
    6749                         continue;
    6750                 }
    6751                 $crossorigin = $processor->get_attribute( 'crossorigin' );
    6752                 if ( null !== $crossorigin ) {
    6753                         continue;
    6754                 }
    6755 
    6756                 if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) {
    6757                         $processor->set_bookmark( 'audio-video-parent' );
    6758                 }
    6759 
    6760                 $processor->set_bookmark( 'resume' );
    6761 
    6762                 $sought = false;
    6763 
    6764                 $is_cross_origin = false;
    6765 
    6766                 foreach ( $cross_origin_tag_attributes[ $tag ] as $attr ) {
    6767                         $url = $processor->get_attribute( $attr );
    6768                         if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) ) {
    6769                                 $is_cross_origin = true;
    6770                         }
    6771 
    6772                         if ( $is_cross_origin ) {
    6773                                 break;
    6774                         }
    6775                 }
    6776 
    6777                 if ( $is_cross_origin ) {
    6778                         if ( 'SOURCE' === $tag ) {
    6779                                 $sought = $processor->seek( 'audio-video-parent' );
    6780 
    6781                                 if ( $sought ) {
    6782                                         $processor->set_attribute( 'crossorigin', 'anonymous' );
    6783                                 }
    6784                         } else {
    6785                                 $processor->set_attribute( 'crossorigin', 'anonymous' );
    6786                         }
    6787 
    6788                         if ( $sought ) {
    6789                                 $processor->seek( 'resume' );
    6790                                 $processor->release_bookmark( 'audio-video-parent' );
    6791                         }
    6792                 }
    6793         }
    6794 
    6795         return $processor->get_updated_html();
    6796 }
    6797 
     6725                return false;
     6726        }
     6727
     6728        header( 'Document-Isolation-Policy: isolate-and-credentialless' );
     6729
     6730        return true;
     6731}
     6732
Note: See TracChangeset for help on using the changeset viewer.