Skip to content

Abilities API: Remove redundant REST run input sanitization - #15684

Closed
gziolo wants to merge 6 commits into
WordPress:trunkfrom
gziolo:abilities-rest-input-preparation
Closed

gziolo wants to merge 6 commits into
WordPress:trunkfrom
gziolo:abilities-rest-input-preparation

Conversation

@gziolo

@gziolo gziolo commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

What

Remove the REST run endpoint's redundant input sanitization pass. The permission callback and WP_Ability::execute() each prepare the original request input independently.

For a successful request with supplied input, the endpoint previously validated and coerced input during REST sanitization, then repeated preparation for the permission check and execution. The run argument now explicitly sets sanitize_callback => null, disabling default REST coercion.

The resulting flow is:

  1. Check that the ability is exposed through REST and that the HTTP method is allowed.
  2. Read raw input from the authoritative source: query parameters for GET/DELETE, or the JSON body for POST.
  3. Normalize, validate, and coerce that input before checking permissions.
  4. Pass the original raw input to WP_Ability::execute(), which performs its own preparation and permission check before execution.

Preparation in the controller is extracted into a private helper. Request parameters remain unchanged, and no request cache is needed. WP_Ability::execute() is unchanged.

Compatibility and errors

  • sanitize_input_for_ability() shipped in WordPress 7.1. It remains callable, is deprecated in 7.2, emits a deprecation notice, and returns its input unchanged. It is no longer registered on the endpoint. Direct callers should account for this behavior change.
  • Preparation errors are returned directly from the permission callback, with HTTP 400 as the fallback and custom statuses preserved. Coercion errors for supplied input now return ability_invalid_input directly instead of a rest_invalid_param wrapper.
  • Hidden abilities and unsupported HTTP methods retain their 404 and 405 responses.

Testing

  • npm run test:php -- --filter=Abilit passes: 1,857 tests, 6,687 assertions. Coding standards, PHP compatibility, and PHPStan checks pass too.
  • Added coverage checks that supplied input and schema defaults are prepared once before the first permission callback. It also checks that invalid input does not replace the 404 error for hidden abilities or the 405 error for a wrong HTTP method.
  • A new test compares each REST run with running the ability directly. It covers GET, POST, and DELETE, with input, with explicit null, and without input (9 cases). The normalize filter is not idempotent, so normalizing the same input twice fails validation. Each case also puts a different value in the source the HTTP method ignores, and sends the same request twice. The response, the permission callback input, and the wp_ability_invoked input must match direct execution.
  • This test passes on trunk. It fails on an earlier version of this branch that stored prepared input on the request, which is the regression reported in review.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code, Codex
Model(s): Claude Opus 5.5 (original implementation)
Used for: Implementation, test updates, regression investigation, review fixes, and PR description updates, iterated on with me.


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.

The run endpoint used to coerce the input during REST sanitization, and then normalize, validate, and coerce it again before the permission check. Now the input is normalized, validated, and coerced once, during sanitization. The permission check uses that input as is. It prepares the input itself only when it is null, because REST sanitization skips absent parameters.

Invalid input is now rejected during sanitization, like other invalid REST parameters. The error is reported as `rest_invalid_param`, with the original error in the details. Abilities hidden from REST and requests with the wrong HTTP method are left to the permission check, so they still get a 404 or 405 error.

`WP_Ability::execute()` is unchanged.

See #66383.
Adds tests that the run endpoint prepares the input once before the permission check, both for input sent with the request and for the schema default. Also adds tests that invalid input does not replace the 404 error for hidden abilities or the 405 error for a wrong HTTP method.

See #66383.
@github-actions

github-actions Bot commented Nov 6, 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 gziolo, jorgefilipecosta, euvoor.

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.

@gziolo gziolo self-assigned this Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 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.

@jorgefilipecosta jorgefilipecosta left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @gziolo,

Thank you for the PR! Besides the unit tests, I tested the endpoint with apiFetch against wp-env everything worked well, the changes look good to me 👍

@euvoor euvoor left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gziolo I tested #15685 at 0fc0606523 against #15684 at 7bda122c48 and its base 4110c7f0a3. The existing --filter=Abilit suite passes.

I found a normalization regression introduced by #15684. With a filter that appends !, input raw, and a string schema with maxLength: 4, direct execution succeeds with raw!. REST now stores the prepared raw!, then passes it into execute(), which normalizes it again. The resulting raw!! fails validation and returns HTTP 500. Before this change, REST succeeds. This reproduces with GET, POST and DELETE.

Two other cases prepare input twice before the first permission callback:

  • Explicit null that remains null after preparation also enters the permission method’s null fallback.
  • Equal values in JSON and query parameters both trigger preparation because value equality cannot distinguish their sources.

All 18 targeted REST cases pass on the base; 12 fail on both proposal heads. Three additional direct prepare_input() error-propagation checks pass. I haven’t found a separate issue in the helper extraction itself.

Could we preserve normalization behavior through the complete REST execution path and add coverage for these cases before committing?

@gziolo gziolo changed the title Abilities API: Prepare REST run input during request sanitization Abilities API: Remove redundant REST run input sanitization Nov 6, 2026
@gziolo

gziolo commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

@euvoor @jorgefilipecosta I’ve revised the approach in edbf3ca.

Looking back at 7.1, coercion happened only in REST sanitization. Trunk now performs coercion in both the permission callback and execute(), making that earlier pass redundant. Moving normalization into sanitization caused the regression you identified: execute() received already-normalized input and normalized it again.

The endpoint now skips input sanitization. The permission callback and execute() independently normalize, validate, and coerce the original raw input. This removes the redundant pass without caching or modifying request parameters.

The public sanitizer shipped in 7.1, so it remains as a deprecated passthrough. Input preparation errors now come directly from the permission callback.

Added regression coverage for non-idempotent normalization, explicit null, absent input, duplicate parameter sources, and repeated dispatches.

@euvoor, could you rerun your targeted cases against this commit? @jorgefilipecosta, could you re-review the revised flow and deprecation handling? The PR description now reflects the implementation.

Replaces the normalization lifecycle test with a test that runs the ability directly and checks that a REST run gives the same result. The permission callback and the execution hook must also get the same input. The test no longer counts internal calls, so it does not depend on how many times the permission check runs.

Also improves the docs for the deprecated input sanitizer and the run arguments.

See #66383.
Replaces the comparison with direct execution by a smaller test. The normalize filter adds a character, and the schema limits the length. So normalizing the input twice fails validation, and the run no longer returns 200.

See #66383.

@euvoor euvoor left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gziolo. I reran the original 18 targeted cases unchanged against both edbf3ca532 and e1d1e27148. All pass, resolving the 12 previous failures.

I also ran 77 additional authorization, input-handling and compatibility cases on e1d1e27148. Those pass too, including conflicting input sources, permission denials, normalization changing the resource between checks, and redispatching the same request after switching to an unauthorized user.

The existing --filter=Abilit suite passes with 1,851 tests and 6,612 assertions on PHP 8.4, single-site.

This resolves my blocking findings. I found no further blocker in the revised flow. Thanks for addressing these.

wporg-sync pushed a commit that referenced this pull request Nov 10, 2026
…oint

The permission callback and `WP_Ability::execute()` already normalize, validate, and coerce input. The `input` argument now sets `sanitize_callback` to `null`, so both receive the original raw input. `sanitize_input_for_ability()` is deprecated and returns its input unchanged.

Follow-up to [62674], [65044].

Developed in #15684.

Props gziolo, jorgefilipecosta, euvoor, justlevine.
Fixes #66383.




git-svn-id: https://develop.svn.wordpress.org/trunk@65368 602fd350-edb4-49c9-b593-d223f7449a82
@github-actions

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 65368
GitHub commit: c636204

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Nov 10, 2026
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Nov 10, 2026
…oint

The permission callback and `WP_Ability::execute()` already normalize, validate, and coerce input. The `input` argument now sets `sanitize_callback` to `null`, so both receive the original raw input. `sanitize_input_for_ability()` is deprecated and returns its input unchanged.

Follow-up to [62674], [65044].

Developed in WordPress/wordpress-develop#15684.

Props gziolo, jorgefilipecosta, euvoor, justlevine.
Fixes #66383.



Built from https://develop.svn.wordpress.org/trunk@65368


git-svn-id: http://core.svn.wordpress.org/trunk@64507 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@gziolo
gziolo deleted the abilities-rest-input-preparation branch November 10, 2026 04:24
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.

3 participants