Skip to content

Media: Only trust core's flag for cross-origin isolation - #15855

Open
adamsilverstein wants to merge 4 commits into
WordPress:trunkfrom
adamsilverstein:fix/66490-dip-flag-only
Open

adamsilverstein wants to merge 4 commits into
WordPress:trunkfrom
adamsilverstein:fix/66490-dip-flag-only

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Trac ticket: https://core.trac.wordpress.org/ticket/66490

Item 1 on the ticket: plugin screens that send their own Document-Isolation-Policy header, like the Elementor editor, were picking up the client-side upload pipeline in every media frame without asking for it.

Claude dug into how the isolation check works and wrote this up:

wp_is_cross_origin_isolated_request() returned true when core's $_wp_cross_origin_isolated flag was set, and also when any Document-Isolation-Policy header was queued in headers_list(). The header check was there for the Gutenberg plugin, which sends the header itself on the editor screens, but it can't tell that apart from a plugin isolating its screen for reasons of its own.

Now only the flag counts, and the only thing that sets it is wp_send_document_isolation_policy_header(). A plugin that wants the pipeline on a screen core does not isolate calls that function instead of sending the header itself. A plugin that sends the header on its own keeps plupload in its media frames, as it did in 7.1.

Companion Gutenberg PR, which sends the header through core's function when it exists: WordPress/gutenberg#86542

Until that lands, a site running the Gutenberg plugin gets classic uploads in the media modal on the editor screens. Nothing breaks, and block editor uploads are unaffected since they go through the upload-media store.

Testing

  • The existing wpEnqueueMediaFrameUpload, wpEnqueueMediaNewUpload and cross-origin isolation tests pass (27 tests). A header sent by a plugin can't be covered in PHPUnit since headers_list() is always empty under the CLI; an e2e test could cover it if reviewers think it's worth adding.
  • PHPStan level 10 and PHPCS are clean on the changed lines.

Manual testing, in Chrome 137+ over HTTPS or localhost:

  1. Add an mu-plugin that isolates a screen core does not, eg. Settings > General:
    add_action( 'load-options-general.php', function () {
        header( 'Document-Isolation-Policy: isolate-and-credentialless' );
    } );
    add_action( 'admin_enqueue_scripts', 'wp_enqueue_media' );
  2. Load Settings > General and check the page source for media-frame-upload. On trunk it's there; with this PR it isn't.
  3. Swap the header() call for wp_send_document_isolation_policy_header() and reload: media-frame-upload is enqueued again.
  4. Without the Gutenberg plugin, open the block editor and upload a JPEG from the media modal: it still goes through /wp/v2/media, /sideload and /finalize.

AI Use

Code and description both came out of a session with 🤖 Claude Code. I will review and test.

🤖 Generated with Claude Code

https://claude.ai/code/session_018vwYjVChF8EN33VU3DGbni

Commit message

Media: Only trust core's flag for cross-origin isolation.

Fix an issue where a plugin screen that sends its own Document-Isolation-Policy header, such as the Elementor editor, got the client-side upload pipeline in every media frame. `wp_is_cross_origin_isolated_request()` also counted any queued Document-Isolation-Policy header, so core could not tell its own isolation apart from a plugin's.

Only the flag set by `wp_send_document_isolation_policy_header()` counts now. A plugin that wants the pipeline on a screen core does not isolate calls that function instead of sending the header itself.

Follow-up to [64588].

See #66490.

wp_is_cross_origin_isolated_request() also returned true when any Document-Isolation-Policy header was queued, so a plugin screen that isolates itself for its own reasons, such as the Elementor editor, got the client-side pipeline in every media frame without asking for it. Uploads there then skipped the plugin's own plupload handling.

Only the flag wp_send_document_isolation_policy_header() sets now counts. A plugin that wants the pipeline on a screen core does not isolate calls that function instead of sending the header itself. The Gutenberg plugin sends the header directly on the editor screens today, so its media modal falls back to classic uploads until it defers to core's function.

See #66490.

Claude-Session: https://claude.ai/code/session_018vwYjVChF8EN33VU3DGbni
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

The docblocks and test comments around the upload pipeline still named wp_set_up_cross_origin_isolation() or any isolated screen as what enables it, which stopped being true once only the flag set by wp_send_document_isolation_policy_header() counted. They also left out that the function has to run before wp_enqueue_media(), so a late call isolates the page without loading the pipeline.

The comments now name the function that sets the flag and state the ordering requirement. No behavior changes.

See #66490.

Claude-Session: https://claude.ai/code/session_018vwYjV5F1fB1H7E4PDwsnV
…function.

The enqueue tests set $_wp_cross_origin_isolated by hand, so nothing would fail if wp_send_document_isolation_policy_header() stopped setting it and the pipeline silently never loaded in production.

The new tests assert wp_is_cross_origin_isolated_request() flips to true only after the header is sent, and stays false when the browser does not support the policy.

See #66490.

Claude-Session: https://claude.ai/code/session_018vwYjV5F1fB1H7E4PDwsnV
@adamsilverstein

Copy link
Copy Markdown
Member Author

I had Claude run thru the manual testing steps from the description, with and without the Gutenberg plugin. I also did some manual smoke testing. Everything behaved as expected.

Here's the report:

Tested on 7.2-beta5 nightly (SQLite, PHP 8.5 built-in server on localhost) in Chrome 157, swapping media.php between trunk and this PR. Gutenberg was 24.3.1 from wordpress.org, then the same build with the one-line change from WordPress/gutenberg#86542 applied. Each upload was a JPEG added to a wp.media frame, watching whether it went through /wp/v2/media, /sideload and /finalize (the pipeline) or async-upload.php (classic). The page was cross-origin isolated in every case.

Without Gutenberg:

  • Trunk, Settings > General isolated with a raw header() call: media-frame-upload enqueued and the modal upload went through the pipeline. This reproduces the bug.
  • PR, same setup: media-frame-upload not enqueued, modal upload went to async-upload.php (step 2).
  • PR, swapping in wp_send_document_isolation_policy_header(): media-frame-upload enqueued again and the upload went through the pipeline (step 3).
  • PR, block editor media modal: pipeline, with 5 sideloads then finalize (step 4).

With Gutenberg 24.3.1:

  • PR, block editor media modal: media-frame-upload not enqueued, upload went to async-upload.php. That's the expected gap until the Gutenberg PR lands - nothing broke.
  • PR, block editor upload through mediaUpload (not the modal): still the pipeline, so editor uploads are unaffected.

With Gutenberg 24.3.1 plus #86542:

  • PR, block editor media modal: media-frame-upload enqueued, upload went through the pipeline again.
  • PR, Settings > General with a raw header() call: still classic, so the plugin doesn't reopen the original bug.

A curl run across all 36 combinations (trunk/PR, Gutenberg off/stable/patched, no header/raw header/core function, on Settings > General and post-new.php) also checked the response header and whether media-frame-upload was in the page source. It matched what the PR intends in every case. No PHP errors or warnings in the debug log.

Looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant