Skip to content

REST API, Meta: Only check meta authorization when a value changes. - #14328

Open
DevinWalker wants to merge 3 commits into
WordPress:trunkfrom
DevinWalker:48426-rest-meta-unchanged-values
Open

DevinWalker wants to merge 3 commits into
WordPress:trunkfrom
DevinWalker:48426-rest-meta-unchanged-values

Conversation

@DevinWalker

@DevinWalker DevinWalker commented Oct 16, 2026 •

Copy link
Copy Markdown

When a post is saved in the block editor, it sends back every registered meta field it got from the REST API, including fields the current user isn't allowed to edit. If one of those fields hits the authorization check, the whole save fails with "Sorry, you are not allowed to edit the X custom field." even though the user never touched that field.

56075 fixed this for single fields that already have a stored value. These cases still fail on trunk:

  • A field with no stored value, sent back with the default the REST API returned for it. Since 58831 compares against the raw stored value, this also fails when a default is registered.
  • A multi field sent back with the same values, because authorization was checked before the values were compared.
  • An empty list or null for a field with no stored value. That goes through the delete path, which checked authorization before checking if there was anything to delete.

What changed

All in WP_REST_Meta_Fields, so it covers post, term, comment and user meta:

  • update_meta_value(): when the user can't edit the field, nothing is stored, and the value matches the default the API returns for it, the update is skipped instead of returning a 403.
  • update_multi_meta_value(): works out what would be added and removed first, returns early when nothing changes, and only checks authorization when something does. The same default check applies.
  • delete_meta_value(): returns early when nothing is stored, before the authorization check.
  • New is_meta_value_same_as_default_value() helper that mirrors what get_value() returns for a field with no stored value.

Users who can edit a field still store the default value, so the 6.7 behavior from #55600 doesn't change. Changing a protected field still returns a 403.

Testing

New tests in tests/phpunit/tests/rest-api/rest-post-meta-fields.php (--group 48426, 21 cases). 8 of them check that changing a protected field is still rejected, so they pass on trunk too.

trunk this PR
--group 48426, single site 13 failures 21/21 pass
--group 48426, multisite 21/21 pass
--group restapi, single site 3649 tests, no failures
--group restapi, multisite 3684 tests, no failures
--group meta, single site / multisite 507 / 533 tests pass

The restapi runs have 4 warnings (test_get_items_wdotorg_unavailable, test_create_item_wdotorg_unreachable and the two test_format_validation_is_applied_if_missing_type tests). They show up on trunk too and aren't related to this change.

PHPCS and PHPStan are clean on the changed files.

I also checked it in the block editor with Playwright on a local wordpress-develop site, using the mu-plugin below. On trunk the save fails, with the patch it saves.

Manual testing

  1. Add this as an mu-plugin:
    <?php
    add_action( 'init', function () {
    	register_post_meta( 'post', '_demo_note', array(
    		'type'          => 'string',
    		'single'        => true,
    		'show_in_rest'  => true,
    		'auth_callback' => function () {
    			return current_user_can( 'edit_posts' );
    		},
    	) );
    	register_post_meta( 'post', '_demo_locked_flag', array(
    		'type'          => 'boolean',
    		'single'        => true,
    		'show_in_rest'  => true,
    		'auth_callback' => function () {
    			return current_user_can( 'this_cap_does_not_exist' );
    		},
    	) );
    } );
  2. Go to Posts > Add New Post.
  3. In the browser console, run wp.data.dispatch( 'core/editor' ).editPost( { meta: { _demo_note: 'Changed' } } );
  4. Click "Save draft".
    • Before: an "Updating failed." notice, and "Show details" says "Sorry, you are not allowed to edit the _demo_locked_flag custom field."
    • After: the draft saves and _demo_note is stored.

Screenshots

Before (trunk) After (this PR)
Trunk: the block editor shows Updating failed, with the details saying Sorry, you are not allowed to edit the _demo_locked_flag custom field. With the patch: the draft saves and the Draft saved snackbar shows.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Finding the ticket, tracing the cause, writing the fix and the PHPUnit tests, the Playwright check, and drafting this description. I reviewed the code and the test results before opening this PR.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

When a post is saved, the block editor sends back every registered meta field it received, including fields the current user is not allowed to edit. [56075] stopped checking authorization for single values that match the stored value, but a save still failed with a 403 when the request included:

* The default value of a field with no stored value. Since [58831] compares against the raw stored value, this also fails when a default is registered.
* The unchanged values of a multi field, because authorization was checked before the values were compared.
* An empty list or `null` for a field with no stored value, which goes through the delete path and its authorization check.

Authorization is now only checked when a value would change. Users who can edit a field still store its default value, as introduced in [58831].

Follow-up to [56075], [58831].

See #48426.
@github-actions

github-actions Bot commented Oct 16, 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 dlocc.

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

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant