From 3f563a1d117c6cacc0c2e68dae4f58c2b6b9daea Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 5 Mar 2026 14:48:56 +0700 Subject: [PATCH 01/14] Media: Skip cross-origin isolation for third-party page builders. Add a check to skip Document-Isolation-Policy when a third-party page builder overrides the block editor via a custom action parameter. DIP isolates the document into its own agent cluster, which blocks same-origin iframe access that these editors rely on. Co-Authored-By: Claude Opus 4.6 --- src/wp-includes/media.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index efb82399ed688..ee15578d4b0af 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6430,6 +6430,14 @@ function wp_set_up_cross_origin_isolation(): void { return; } + // Skip when a third-party page builder overrides the block editor. + // DIP isolates the document into its own agent cluster, + // which blocks same-origin iframe access that these editors rely on. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) { + return; + } + // Cross-origin isolation is not needed if users can't upload files anyway. if ( ! current_user_can( 'upload_files' ) ) { return; From a65f438a5ae68bbd3402a21f97f35c08adbc546b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 15:53:30 -0700 Subject: [PATCH 02/14] Media: Disable client-side processing on non-secure origins SharedArrayBuffer is unavailable on non-HTTPS, non-localhost origins, so cross-origin isolation headers have no effect. Default the feature to disabled in those contexts to avoid unnecessary console warnings. --- src/wp-includes/media.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 8e4f67a3491b2..79e2dc59377e2 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6371,14 +6371,16 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { * @return bool Whether client-side media processing is enabled. */ function wp_is_client_side_media_processing_enabled(): bool { + $enabled = ( is_ssl() || 'localhost' === $_SERVER['HTTP_HOST'] ); + /** * Filters whether client-side media processing is enabled. * * @since 7.0.0 * - * @param bool $enabled Whether client-side media processing is enabled. Default true. + * @param bool $enabled Whether client-side media processing is enabled. Default true if SSL or localhost. */ - return (bool) apply_filters( 'wp_client_side_media_processing_enabled', true ); + return (bool) apply_filters( 'wp_client_side_media_processing_enabled', $enabled ); } /** From 1c748210b9d05d1dae4a0e344784e084fe247273 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 16:04:00 -0700 Subject: [PATCH 03/14] Media: Use multiline comment style per WP standards --- src/wp-includes/media.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 79e2dc59377e2..3d364a189971a 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6456,9 +6456,11 @@ function wp_set_up_cross_origin_isolation(): void { return; } - // Skip when a third-party page builder overrides the block editor. - // DIP isolates the document into its own agent cluster, - // which blocks same-origin iframe access that these editors rely on. + /* + * Skip when a third-party page builder overrides the block editor. + * DIP isolates the document into its own agent cluster, + * which blocks same-origin iframe access that these editors rely on. + */ // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) { return; From 2f0421f6d61f053a2d6e46b8861edbf19d5385f6 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 16:05:43 -0700 Subject: [PATCH 04/14] Media: Document page builder skip in docblock --- src/wp-includes/media.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 3d364a189971a..70cf29371f02e 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6439,6 +6439,10 @@ function wp_get_chromium_major_version(): ?int { * media processing in the editor. Uses Document-Isolation-Policy * on supported browsers (Chromium 137+). * + * Skips setup when a third-party page builder overrides the block + * editor via a custom `action` query parameter, as DIP would block + * same-origin iframe access that these editors rely on. + * * @since 7.0.0 */ function wp_set_up_cross_origin_isolation(): void { From e7298c137b9d6e977a7d4f3853ec1b8746da4ed2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 16:18:19 -0700 Subject: [PATCH 05/14] Media: Add tests for secure origin check Add unit tests verifying that client-side media processing is disabled on non-secure, non-localhost origins and enabled on localhost regardless of SSL. --- .../tests/media/wpCrossOriginIsolation.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index 31f2e85975ee0..e603d8fd9862b 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -6,6 +6,7 @@ * @group media * @covers ::wp_set_up_cross_origin_isolation * @covers ::wp_start_cross_origin_isolation_output_buffer + * @covers ::wp_is_client_side_media_processing_enabled */ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { @@ -16,9 +17,33 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { */ private $original_user_agent; + /** + * Original HTTP_HOST value. + * + * @var string|null + */ + private $original_http_host; + + /** + * Original HTTPS value. + * + * @var string|null + */ + private $original_https; + + /** + * Original $_GET['action'] value. + * + * @var string|null + */ + private $original_get_action; + public function set_up() { parent::set_up(); $this->original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; + $this->original_http_host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : null; + $this->original_https = isset( $_SERVER['HTTPS'] ) ? $_SERVER['HTTPS'] : null; + $this->original_get_action = isset( $_GET['action'] ) ? $_GET['action'] : null; } public function tear_down() { @@ -28,6 +53,24 @@ public function tear_down() { $_SERVER['HTTP_USER_AGENT'] = $this->original_user_agent; } + if ( null === $this->original_http_host ) { + unset( $_SERVER['HTTP_HOST'] ); + } else { + $_SERVER['HTTP_HOST'] = $this->original_http_host; + } + + if ( null === $this->original_https ) { + unset( $_SERVER['HTTPS'] ); + } else { + $_SERVER['HTTPS'] = $this->original_https; + } + + if ( null === $this->original_get_action ) { + unset( $_GET['action'] ); + } else { + $_GET['action'] = $this->original_get_action; + } + // Clean up any output buffers started during tests. while ( ob_get_level() > 1 ) { ob_end_clean(); @@ -124,6 +167,32 @@ public function test_does_not_start_output_buffer_for_safari() { $this->assertSame( $level_before, $level_after, 'Output buffer should not be started for Safari.' ); } + /** + * @ticket 64803 + */ + public function test_client_side_processing_disabled_on_non_secure_origin() { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = ''; + + $this->assertFalse( + wp_is_client_side_media_processing_enabled(), + 'Client-side media processing should be disabled on non-secure, non-localhost origins.' + ); + } + + /** + * @ticket 64803 + */ + public function test_client_side_processing_enabled_on_localhost() { + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['HTTPS'] = ''; + + $this->assertTrue( + wp_is_client_side_media_processing_enabled(), + 'Client-side media processing should be enabled on localhost.' + ); + } + /** * This test must run in a separate process because the output buffer * callback sends HTTP headers via header(), which would fail in the From 85d6e62e24b40b10a6da49f98be932e0ba563913 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 16:20:08 -0700 Subject: [PATCH 06/14] Media: Remove phpcs nonce verification ignore The NonceVerification.Recommended suppression is not needed here since $_GET['action'] is only used for a simple string comparison, not for any state-changing operation. --- src/wp-includes/media.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 70cf29371f02e..9dbadc63eb5f5 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6465,7 +6465,6 @@ function wp_set_up_cross_origin_isolation(): void { * DIP isolates the document into its own agent cluster, * which blocks same-origin iframe access that these editors rely on. */ - // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) { return; } From a1e630e4ade7e0ac36faf81272b26039e8029a30 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 10 Mar 2026 16:22:38 -0700 Subject: [PATCH 07/14] Update src/wp-includes/media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 9dbadc63eb5f5..c8c716eaf9b56 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6371,6 +6371,7 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { * @return bool Whether client-side media processing is enabled. */ function wp_is_client_side_media_processing_enabled(): bool { + // This is due to SharedArrayBuffer requiring a secure context. $enabled = ( is_ssl() || 'localhost' === $_SERVER['HTTP_HOST'] ); /** From 9ee5ac140be55e15c2c17b8ac0a831b625359cbb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 10 Mar 2026 16:25:31 -0700 Subject: [PATCH 08/14] Use class property type hints and null coalescing operator --- .../tests/media/wpCrossOriginIsolation.php | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index e603d8fd9862b..4fe5723bdc426 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -12,38 +12,30 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { /** * Original HTTP_USER_AGENT value. - * - * @var string|null */ - private $original_user_agent; + private ?string $original_user_agent; /** * Original HTTP_HOST value. - * - * @var string|null */ - private $original_http_host; + private ?string $original_http_host; /** * Original HTTPS value. - * - * @var string|null */ - private $original_https; + private ?string $original_https; /** * Original $_GET['action'] value. - * - * @var string|null */ - private $original_get_action; + private ?string $original_get_action; public function set_up() { parent::set_up(); - $this->original_user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null; - $this->original_http_host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : null; - $this->original_https = isset( $_SERVER['HTTPS'] ) ? $_SERVER['HTTPS'] : null; - $this->original_get_action = isset( $_GET['action'] ) ? $_GET['action'] : null; + $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; + $this->original_http_host = $_SERVER['HTTP_HOST'] ?? null; + $this->original_https = $_SERVER['HTTPS'] ?? null; + $this->original_get_action = $_GET['action'] ?? null; } public function tear_down() { From 7af02dc3588d870c49491f0dcefb2fde826afbc1 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 10 Mar 2026 16:50:29 -0700 Subject: [PATCH 09/14] Update media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index c8c716eaf9b56..b50924dabc124 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6394,7 +6394,7 @@ function wp_set_client_side_media_processing_flag(): void { return; } - wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true', 'before' ); + wp_add_inline_script( 'wp-block-editor', 'window.__clientSideMediaProcessing = true;', 'before' ); $chromium_version = wp_get_chromium_major_version(); From ec1490ca2131a5345be66ab79439e3d244b8bfa8 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 10 Mar 2026 16:52:21 -0700 Subject: [PATCH 10/14] Update media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index b50924dabc124..9310cd6bf6860 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6372,7 +6372,7 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { */ function wp_is_client_side_media_processing_enabled(): bool { // This is due to SharedArrayBuffer requiring a secure context. - $enabled = ( is_ssl() || 'localhost' === $_SERVER['HTTP_HOST'] ); + $enabled = ( is_ssl() || 'localhost' === strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) ); /** * Filters whether client-side media processing is enabled. From e26a9c3360dbd8a421e1ae5054d6f798b0f4c7c5 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 10 Mar 2026 16:52:51 -0700 Subject: [PATCH 11/14] Update media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 9310cd6bf6860..94ce30013cc1c 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6379,7 +6379,7 @@ function wp_is_client_side_media_processing_enabled(): bool { * * @since 7.0.0 * - * @param bool $enabled Whether client-side media processing is enabled. Default true if SSL or localhost. + * @param bool $enabled Whether client-side media processing is enabled. Default true if the page is served in a secure context. */ return (bool) apply_filters( 'wp_client_side_media_processing_enabled', $enabled ); } From afacb9eaae35213a05c8d30622cf83797344a95d Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 10 Mar 2026 16:54:33 -0700 Subject: [PATCH 12/14] Update media.php Co-authored-by: Weston Ruter --- src/wp-includes/media.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 94ce30013cc1c..1dd671db05e75 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6372,7 +6372,8 @@ function wp_get_image_editor_output_format( $filename, $mime_type ) { */ function wp_is_client_side_media_processing_enabled(): bool { // This is due to SharedArrayBuffer requiring a secure context. - $enabled = ( is_ssl() || 'localhost' === strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) ); + $host = strtolower( (string) strtok( $_SERVER['HTTP_HOST'] ?? '', ':' ) ); + $enabled = ( is_ssl() || 'localhost' === $host || str_ends_with( $host, '.localhost' ) ); /** * Filters whether client-side media processing is enabled. From b122f7fca6377665730ce04dd502f3e61d62e4ae Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 17:24:15 -0700 Subject: [PATCH 13/14] Fix sideload tests for non-secure contexts The sideload route is only registered when client-side media processing is enabled, which now requires SSL or localhost. Force-enable the filter in affected tests so routes register correctly in CI. --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 5 +++++ tests/phpunit/tests/rest-api/rest-schema-setup.php | 3 +++ 2 files changed, 8 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 93cd4211c93ba..a02fa99660c70 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3162,6 +3162,7 @@ static function ( $data ) use ( &$captured_data ) { * @requires function imagejpeg */ public function test_sideload_scaled_image() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); wp_set_current_user( self::$author_id ); // First, create an attachment. @@ -3215,6 +3216,7 @@ public function test_sideload_scaled_image() { * @requires function imagejpeg */ public function test_sideload_scaled_image_requires_auth() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); wp_set_current_user( self::$author_id ); // Create an attachment. @@ -3244,6 +3246,7 @@ public function test_sideload_scaled_image_requires_auth() { * @ticket 64737 */ public function test_sideload_route_includes_scaled_enum() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); $server = rest_get_server(); $routes = $server->get_routes(); @@ -3266,6 +3269,7 @@ public function test_sideload_route_includes_scaled_enum() { * @requires function imagejpeg */ public function test_sideload_scaled_unique_filename() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); wp_set_current_user( self::$author_id ); // Create an attachment. @@ -3300,6 +3304,7 @@ public function test_sideload_scaled_unique_filename() { * @requires function imagejpeg */ public function test_sideload_scaled_unique_filename_conflict() { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); wp_set_current_user( self::$author_id ); // Create the first attachment. diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 0e0e00b934359..01dad836981bc 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -16,6 +16,9 @@ class WP_Test_REST_Schema_Initialization extends WP_Test_REST_TestCase { public function set_up() { parent::set_up(); + // Ensure client-side media processing is enabled so the sideload route is registered. + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = new Spy_REST_Server(); From 119efb67ceac801c70ce3a7d42d6331a23d3bf10 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 10 Mar 2026 18:00:14 -0700 Subject: [PATCH 14/14] Reinitialize REST server in sideload tests The parent set_up() creates and initializes the REST server before individual test methods run, so the filter alone is not enough. Reset the server global and re-fire rest_api_init after enabling the filter. --- .../rest-api/rest-attachments-controller.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index a02fa99660c70..7fa4545a38d9c 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3163,6 +3163,11 @@ static function ( $data ) use ( &$captured_data ) { */ public function test_sideload_scaled_image() { add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + // Reinitialize REST server so the sideload route is registered. + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + wp_set_current_user( self::$author_id ); // First, create an attachment. @@ -3217,6 +3222,11 @@ public function test_sideload_scaled_image() { */ public function test_sideload_scaled_image_requires_auth() { add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + // Reinitialize REST server so the sideload route is registered. + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + wp_set_current_user( self::$author_id ); // Create an attachment. @@ -3247,6 +3257,11 @@ public function test_sideload_scaled_image_requires_auth() { */ public function test_sideload_route_includes_scaled_enum() { add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + // Reinitialize REST server so the sideload route is registered. + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + $server = rest_get_server(); $routes = $server->get_routes(); @@ -3270,6 +3285,11 @@ public function test_sideload_route_includes_scaled_enum() { */ public function test_sideload_scaled_unique_filename() { add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + // Reinitialize REST server so the sideload route is registered. + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + wp_set_current_user( self::$author_id ); // Create an attachment. @@ -3305,6 +3325,11 @@ public function test_sideload_scaled_unique_filename() { */ public function test_sideload_scaled_unique_filename_conflict() { add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + // Reinitialize REST server so the sideload route is registered. + global $wp_rest_server; + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + wp_set_current_user( self::$author_id ); // Create the first attachment.