Make WordPress Core

Opened 2 months ago

Last modified 3 days ago

#65493 assigned defect (bug)

Editor: Broaden the Document-Isolation-Policy header to all admin pages

Reported by: adamsilverstein Owned by: adamsilverstein
Priority: normal Milestone: Awaiting Review
Component: Media Version:
Severity: normal Keywords: has-patch has-test-info has-unit-tests 2nd-opinion dev-feedback
Cc: Focuses:

Description (last modified by adamsilverstein)

The block editor sends the Document-Isolation-Policy (DIP) header to enable SharedArrayBuffer for WebAssembly-based client-side media processing in Chromium 137+. Today the header is hooked onto only four screens: load-post.php, load-post-new.php, load-site-editor.php, and load-widgets.php.

Because the header is scoped that narrowly, navigations from an editor screen to a sibling admin screen (site editor sub-routes, template operations, pattern editing) load without the header. Chromium 137+ then places those documents in a different browser agent cluster from the editor. The agent-cluster mismatch breaks cross-window communication (window.opener, popup references) and SharedArrayBuffer access that client-side media processing relies on.

This is the same problem that currently forces several editor E2E suites to disable client-side media processing wholesale.

Proposed change

Send the DIP header on all admin pages by hooking wp_set_up_cross_origin_isolation() onto admin_init instead of the four load-* screen actions. Keeping every admin navigation in the same agent cluster preserves cross-window communication.

The block-editor screen gate is no longer needed. Cross-origin isolation is only relevant for users who can upload media, so the screen check is replaced by a single capability check:

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

This is also cheaper than resolving the current screen on every admin request.

The existing escape hatch for third-party page builders that override the block editor via a custom action query parameter is retained:

<?php
if ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) {
        return;
}

Scope

  • Admin-only. The front-end preview is intentionally out of scope; DIP remains an admin-only concern.
  • The classic-theme site-preview skip is a separate change (see #65399).

Affected functions

  • wp_set_up_cross_origin_isolation() in src/wp-includes/media.php
  • Hook registration in src/wp-includes/default-filters.php

How to test

  1. Run Chromium 137+ against a build with the patch applied.
  2. As a user with the upload_files capability, confirm the Document-Isolation-Policy: isolate-and-credentialless response header is present on /wp-admin/index.php, /wp-admin/site-editor.php, /wp-admin/edit.php, and /wp-admin/post.php?post=…&action=edit.
  3. Sign in as a Subscriber (no upload_files) and confirm the header is not sent on any admin page.
  4. Visit a third-party editor URL with ?action=elementor (or similar) and confirm the header is not sent.
  5. Run vendor/bin/phpunit tests/phpunit/tests/media/wpCrossOriginIsolation.php.

Patch

Pull request: https://github.com/WordPress/wordpress-develop/pull/11298

Upstream Gutenberg pull request:
https://github.com/WordPress/gutenberg/pull/76662

Change History (14)

#1 @adamsilverstein
2 months ago

  • Description modified (diff)

#2 follow-up: @adamsilverstein
2 months ago

The existing escape hatch for third-party page builders that override the block editor via a custom action query parameter is retained:

I don't think this is required any longer. We fixed the compatibility issue with Elementor in https://github.com/elementor/elementor/issues/35975, so i believe we can remove that, although its worth checking compatibility with other popular page builders like beaver builder, wp bakery and divi.

#3 in reply to: ↑ 2 @adamsilverstein
2 months ago

Replying to adamsilverstein:

The existing escape hatch for third-party page builders that override the block editor via a custom action query parameter is retained:

I don't think this is required any longer. We fixed the compatibility issue with Elementor in https://github.com/elementor/elementor/issues/35975, so i believe we can remove that, although its worth checking compatibility with other popular page builders like beaver builder, wp bakery and divi.

Page-builder compatibility Research

The escape hatch is kept because some builders take over an admin editor screen
via a custom action value and embed a same-origin front-end preview iframe
that they access synchronously — which Document-Isolation-Policy breaks when only
the admin frame is isolated.

At risk — admin chrome + synchronous same-origin preview iframe:

  • Elementor (action=elementor) no longer strictly needs the hatch: as of v4.x it sends the DIP header on both its editor frame and its preview iframe (elementor/elementor#35976), so it self-isolates. The hatch is harmless to it.
  • Brizy (action=in-front-editor) still relies on the hatch and would regress without it — no equivalent fix shipped.
  • WPBakery front-end editor (vc_action=vc_inline) and Visual Composer standalone (vcv-action=frontend) match the same pattern but trigger on a different query parameter, so the action-based hatch never covered them.

Unaffected — editor chrome served on the front end (admin header never reaches it):

Unaffected — backend metabox on the standard action=edit screen (already coexists with DIP):

Alternate approaches considered

  1. Send DIP on front-end preview pages too (via template_redirect). Earlier iterations added a wp_set_up_cross_origin_isolation_for_preview() helper so editor → preview popups stayed in the same agent cluster. Rejected: DIP is kept admin-only; front-end isolation is out of scope and the upstream Gutenberg change dropped this path. No core counterpart remains.
  2. Replace the action !== 'edit' heuristic with editor-replacement detection (e.g. use_block_editor_for_post() === false / the replace_editor filter). This would protect all editor-replacing builders regardless of query parameter — including WPBakery and Visual Composer, which the action-based check misses. Deferred: it is a larger change that needs the screen/post context restored, and the current heuristic plus builders self-isolating (as Elementor now does) covers the practical cases. Worth revisiting as a follow-up.
  3. Remove the escape hatch entirely (platform-forward) — send DIP on all admin editors and require builders to self-isolate as Elementor did. Rejected: it would regress Brizy and any other builder that embeds a same-origin preview iframe without having shipped a fix.
  4. Keep hooking only the specific editor screens (the status quo of load-post.php / load-post-new.php / load-site-editor.php / load-widgets.php). Rejected: this is the root cause — sibling admin navigations load without DIP and fall into a different agent cluster, which is the bug this ticket fixes.

#4 @wildworks
8 weeks ago

  • Component EditorMedia
  • Milestone Awaiting Review7.1

#5 @khokansardar
8 weeks ago

Patch testing report

Patch / PR tested

Environment

WordPress: 7.1-alpha-62161-src
PHP: 8.2.18 (Docker)
MySQL: 8.0.36
OS: macOS 26.5.1
Browser: Chromium 149 (Chrome/149.0.0.0), desktop viewport
Local wordpress-develop @ http://localhost:8889

Steps

  1. Checked out PR #11298 and verified it is a PHP-only change (no build needed for -src).
  2. Ran the unit test file: vendor/bin/phpunit tests/phpunit/tests/media/wpCrossOriginIsolation.php.
  3. As an Administrator (has upload_files), fetched several admin screens and inspected the Document-Isolation-Policy (DIP) response header: index.php, edit.php, site-editor.php, upload.php, options-general.php, and post.php?post=…&action=edit.
  4. Tested the third-party page-builder escape hatch by requesting admin pages with custom action values: ?action=elementor and ?action=in-front-editor (Brizy), comparing against ?action=edit and no action.
  5. Created a Subscriber (no upload_files), signed in via an isolated browser context, and checked the DIP header on admin pages.

Results

  • Unit tests: pass — OK (20 tests, 22 assertions), including the renamed test_returns_early_when_user_cannot_upload().
  • Header on non-editor admin page (index.php / Dashboard): pass — Document-Isolation-Policy: isolate-and-credentialless now present (on trunk it is absent here, since the header was previously hooked only to load-post/load-post-new/load-site-editor/load-widgets). This is the core fix.
  • Header on editor + sibling admin screens: pass — present on edit.php, site-editor.php, upload.php, options-general.php, and post.php?post=…&action=edit.
  • Escape hatch (custom action): pass — header NOT sent on index.php?action=elementor or ?action=in-front-editor; correctly still sent for ?action=edit and requests with no action param.
  • Subscriber (no upload_files): pass — header NOT sent on any admin page (index.php, profile.php).

Conclusion
PR #11298 broadens the Document-Isolation-Policy header from the four block-editor load-* screens to all admin pages by hooking wp_set_up_cross_origin_isolation() on admin_init, replacing the get_current_screen()/is_block_editor() gate with a current_user_can( 'upload_files' ) capability check, while retaining the existing action !== 'edit' escape hatch for page builders. Observed behaviour matches the ticket exactly: every admin navigation for an upload-capable user now stays in the same agent cluster, non-capable users and custom-action page builders are correctly excluded. The change is minimal and scoped to the ticket — 3 files (+14/−20), no signature or return-shape changes, internal hook registration only, so no backward-compatibility concern for the public API. The per-request cost is lower than before (capability check vs. screen resolution). Recommend commit.

#6 @khokansardar
8 weeks ago

  • Keywords has-test-info added

This ticket was mentioned in PR #11298 on WordPress/wordpress-develop by @adamsilverstein.


8 weeks ago
#7

  • Keywords has-unit-tests added

## Summary

Send the Document-Isolation-Policy (DIP) header on all admin pages (via admin_init) instead of only on the four block-editor screens (load-post.php, load-post-new.php, load-site-editor.php, load-widgets.php).

  • Replace the block-editor screen gate in wp_set_up_cross_origin_isolation() with an upload_files capability check.
  • Retain the existing escape hatch for third-party page builders that take over an admin editor screen via a custom action query parameter (see "Page-builder compatibility" below).

## Why

When the editor sends the DIP header but sibling admin navigations (site editor sub-routes, template and pattern operations) load without it, Chromium 137+ places them in different agent clusters. The mismatch breaks cross-window communication (window.opener, popup references) and SharedArrayBuffer access required for WebAssembly-based client-side media processing. Broadening the header to every admin page keeps all admin navigations in the same agent cluster.

The block-editor screen gate is no longer needed: cross-origin isolation is only relevant for users who can upload media, so an upload_files capability check is sufficient and cheaper than resolving the current screen on every admin request.

## Scope

  • Admin-only. The front-end preview is intentionally out of scope; DIP remains an admin-only concern. This mirrors the upstream Gutenberg PR.
  • The classic-theme site-preview skip is handled separately in #12004 (backport of GB #78404).

## Page-builder compatibility

The action !== 'edit' escape hatch is retained. It skips DIP when a third-party builder takes over an admin editor screen via a custom action value, because those builders embed a same-origin front-end preview iframe and access it synchronously — which DIP breaks when only the admin frame is isolated.

At risk — admin chrome + synchronous same-origin preview iframe:

  • Elementor (action=elementor) no longer strictly needs the hatch: as of v4.x it sends the DIP header on both its editor frame and its preview iframe itself (elementor/elementor#35976), so it self-isolates. The hatch is harmless to it.
  • Brizy (action=in-front-editor) still relies on the hatch and would regress without it — no equivalent fix shipped.
  • WPBakery front-end editor (vc_action=vc_inline) and Visual Composer standalone (vcv-action=frontend) match the same pattern but trigger on a different query parameter, so the action-based hatch never covered them.

Unaffected — editor chrome served on the front end (admin header never reaches it): Beaver Builder (?fl_builder), Divi Visual Builder (?et_fb=1), Oxygen (?ct_builder=true), Bricks (?bricks=run), Cornerstone (?cornerstone=1), Avada Live (?fb-edit=1), Thrive Architect (?tve=true), Breakdance (?breakdance=builder), Zion Builder.

Unaffected — backend metabox on the standard action=edit screen (already coexists with DIP): WPBakery backend editor, Divi Classic Builder, SiteOrigin Page Builder, Avada backend builder.

## Alternate approaches considered

  1. Send DIP on front-end preview pages too (via template_redirect). Earlier iterations of this backport added a wp_set_up_cross_origin_isolation_for_preview() helper so editor → preview popups stayed in the same agent cluster. Rejected: DIP is kept admin-only; front-end isolation is out of scope and the upstream Gutenberg change dropped this path, leaving no core counterpart. (Removed from this PR.)
  2. Replace the action !== 'edit' heuristic with editor-replacement detection (e.g. use_block_editor_for_post() === false / the replace_editor filter). This would protect all editor-replacing builders regardless of query parameter — including WPBakery and Visual Composer, which the action-based check misses. Deferred: larger change that needs the screen/post context restored, and the current heuristic plus builders self-isolating (as Elementor now does) covers the practical cases. Worth a follow-up.
  3. Remove the escape hatch entirely (platform-forward) — send DIP on all admin editors and require builders to self-isolate as Elementor did. Rejected: would regress Brizy and any other builder embedding a same-origin preview iframe without a shipped fix.
  4. Keep hooking only the specific editor screens (load-post.php etc., the status quo). Rejected: this is the root cause — sibling admin navigations load without DIP and fall into a different agent cluster, which is the bug this PR fixes.

## Changes

### src/wp-includes/default-filters.php

  • Hook wp_set_up_cross_origin_isolation on admin_init instead of the four load-* screen actions.

### src/wp-includes/media.php

  • wp_set_up_cross_origin_isolation() — drop the get_current_screen() block-editor gate; gate on current_user_can( 'upload_files' ) instead. The third-party page-builder escape hatch is unchanged.

### tests/phpunit/tests/media/wpCrossOriginIsolation.php

  • Replace test_returns_early_when_no_screen() with test_returns_early_when_user_cannot_upload().

## Test plan

  • [ ] vendor/bin/phpunit tests/phpunit/tests/media/wpCrossOriginIsolation.php — all tests pass.
  • [ ] In Chromium 137+ as a user with upload_files, confirm the Document-Isolation-Policy: isolate-and-credentialless response header is present on /wp-admin/index.php, /wp-admin/site-editor.php, /wp-admin/edit.php, and /wp-admin/post.php?post=…&action=edit.
  • [ ] Sign in as a Subscriber (no upload_files) and confirm the header is not sent on any admin page.
  • [ ] Visit a third-party editor URL with ?action=elementor (or similar custom action) and confirm the header is not sent.

## Related

#8 @adamsilverstein
8 weeks ago

Thanks for reviewing @khokansardar

#9 @adamsilverstein
8 weeks ago

  • Owner set to adamsilverstein
  • Status newassigned

@adamsilverstein commented on PR #11298:


8 weeks ago
#10

Code change looks reasonable. Probably should land early to get feedback in case it breaks plugins.

plugin breakage would be my main concern.

I guess it would be ok to commit before beta with a clear understanding that a revert might be needed after collecting feedback.

@adamsilverstein commented on PR #11298:


5 weeks ago
#11

I wonder if we should make this plugin only for a release cycle before backporting to core?

#12 @adamsilverstein
5 weeks ago

  • Keywords 2nd-opinion dev-feedback added
  • Milestone 7.1Awaiting Review

@apeatling commented on PR #11298:


10 days ago
#13

Coming here to add Miles to the list of things that will break with this change. I iframe the standard block editor from a Miles created admin page, and DIP splits the framed editor into a different agent cluster than its same-origin parent, the action and replace_editor skips don't help because we're rendering the real editor at action=edit.

@adamsilverstein commented on PR #11298:


3 days ago
#14

Coming here to add [bymiles.ai Miles] to the list of things that will break with this change. I iframe the standard block editor from a Miles created admin page, and DIP splits the framed editor into a different agent cluster than its same-origin parent, the action and replace_editor skips don't help because I'm rendering the real editor at action=edit.

Hi @apeatling - thanks for the feedback and example.

Rgarding your current compatibility issues - have you considered adopting DIP for your Miles Parent frame? If you emit Document-Isolation-Policy: isolate-and-credentialless while mirroring core's guards you should get full compatibility (Elementor did something similar).

Note: See TracTickets for help on using tickets.