Repository navigation
💥 feat: requests that change something carry a nonce (3.0) - #129
Merged
Merged
Conversation
gfazioli
added a commit
that referenced
this pull request
Oct 6, 2026
- a pages/ csrf() is the opt-out only when declared, public and callable with no arguments: a csrf($token) of the page's own threw during menu setup; - migrate:to-v3 checks each POST form on its own (one form with the field hid another without it), and the pages/ classes that print their own forms.
gfazioli
added a commit
that referenced
this pull request
Oct 6, 2026
…ings A pages/ class that opts out with a public csrf() returning false is not listed, and in a view only its HTML counts: a POST form printed as an example inside a PHP string (htmlentities(), as the Options boilerplate does) is not one a browser submits. A pages/ class's returned strings still count.
gfazioli
added a commit
that referenced
this pull request
Oct 6, 2026
- the nonce may come in the query string or, for a fetch() with a JSON body
(no $_POST), in an X-WPBones-Nonce header; a logged Ajax action reads it
from $_REQUEST, as check_ajax_referer() does;
- WordPress's own forms that post to the page they are on, each with a nonce
of its own, go through: Screen Options and the filesystem credentials form;
- migrate:to-v3 finds the framework's Ajax provider under the name a use
statement gives it (make:ajax and the boilerplates import it as
ServiceProvider), and lists every stripslashes()/wp_unslash() in a file
that calls useHTTPPost(), which now unslashes (Scotty decodes JSON after
stripslashes: a quote would come back null);
- make:ajax writes $nonceHash = '{ClassName}' (it wrote '', which 3.0 refuses);
- the opt-out is described as what it is, for a page that checks a nonce of
its own: wp-admin asks for a logged-in browser first, so it is no door for
requests from elsewhere;
- route and pages/ load callbacks hang on the same hook name as the guard;
- tests: every menu item is guarded, a pages/ POST without the nonce is
refused (removing either guard used to pass).
Breaking, for 3.0 (audit S4, S5): - every request to a WP Bones admin page (menu item, route page, pages/ class) that is not a GET or a HEAD carries the plugin's nonce, in the _wpbones_nonce field $plugin->csrfField() prints; Routing\Csrf checks it on load, before any load callback, and again before the page renders, and answers wp_nonce_ays(). A route that takes requests from elsewhere says 'csrf' => false; a pages/ class, a public csrf() returning false. Up to 2.x store()/update()/destroy() and the load callbacks ran for a form posted from another site. - a logged Ajax action of a provider without $nonceHash (or $nonceKey) refuses every request, with a notice; up to 2.x its nonce check returned true. useHTTPPost() returns unslashed values. - Request::verifyNonce() reads a missing _wpnonce as a failed check, not an undefined index.
… open Ajax providers The views whose POST forms do not print $plugin->csrfField(), and the direct children of WordPressAjaxServiceProvider with logged actions and no $nonceHash (a child of the plugin's own base class may inherit one, so it is not listed).
- a pages/ csrf() is the opt-out only when declared, public and callable with no arguments: a csrf($token) of the page's own threw during menu setup; - migrate:to-v3 checks each POST form on its own (one form with the field hid another without it), and the pages/ classes that print their own forms.
…ings A pages/ class that opts out with a public csrf() returning false is not listed, and in a view only its HTML counts: a POST form printed as an example inside a PHP string (htmlentities(), as the Options boilerplate does) is not one a browser submits. A pages/ class's returned strings still count.
- the nonce may come in the query string or, for a fetch() with a JSON body
(no $_POST), in an X-WPBones-Nonce header; a logged Ajax action reads it
from $_REQUEST, as check_ajax_referer() does;
- WordPress's own forms that post to the page they are on, each with a nonce
of its own, go through: Screen Options and the filesystem credentials form;
- migrate:to-v3 finds the framework's Ajax provider under the name a use
statement gives it (make:ajax and the boilerplates import it as
ServiceProvider), and lists every stripslashes()/wp_unslash() in a file
that calls useHTTPPost(), which now unslashes (Scotty decodes JSON after
stripslashes: a quote would come back null);
- make:ajax writes $nonceHash = '{ClassName}' (it wrote '', which 3.0 refuses);
- the opt-out is described as what it is, for a page that checks a nonce of
its own: wp-admin asks for a logged-in browser first, so it is no door for
requests from elsewhere;
- route and pages/ load callbacks hang on the same hook name as the guard;
- tests: every menu item is guarded, a pages/ POST without the nonce is
refused (removing either guard used to pass).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For 3.0, on
v3: requests that change something carry a nonce (audit S4, S5).💥 Admin pages
Up to 2.x, a WP Bones admin page mapped the HTTP verb to
store(),update()anddestroy(), and ran itsloadcallbacks for any request. A form posted from another site with an administrator's cookies did both.Since 3.0, every request to a WP Bones admin page that is not a GET or a HEAD carries the plugin's nonce. That covers menu items, route pages and
pages/classes.<?php echo $plugin->csrfField(); ?>, or{!! $plugin->csrfField() !!}in Blade. The field is_wpbones_nonce, not_wpnonce, so a form can keep awp_nonce_field()of its own beside it.Routing\Csrfruns onload-{hook}atPHP_INT_MIN, before anyloadcallback, and again before the page renders.wp_nonce_ays(), "The link you followed has expired." with a 403.X-WPBones-Nonceheader, for afetch()with a JSON body.'csrf' => falsein a route or menu item'sroute, or a public, argument-freecsrf()returningfalseon apages/class. It is not a door for requests from other sites: wp-admin requires a logged-in browser first.💥 Ajax
loggedaction of a provider without$nonceHash(or$nonceKey) refuses every request with a 403, and a_doing_it_wrong()names the provider. Up to 2.x its nonce check returnedtrue.$_REQUEST, ascheck_ajax_referer()does.make:ajaxsets$nonceHash = '{ClassName}'. It used to write'', which 3.0 refuses.trustedandnotLoggedare public by design and unchanged.useHTTPPost()returns unslashed values.Request::verifyNonce()reads a missing_wpnonceas a failed check, not an undefined index.✨
migrate:to-v3It also lists:
csrfField();WordPressAjaxServiceProvider, also under the name ause … asgives it, withloggedactions and no$nonceHash. A child of the plugin's own base class may inherit one, so it is not listed;stripslashes()orwp_unslash()in a file that callsuseHTTPPost(). That value now comes unslashed, and a second pass corrupts quotes and backslashes: Scotty's preferences JSON would decode tonull.Each form is checked on its own, in the views and in
pages/classes. A page that opts out is not listed, and neither is a form printed as an example inside a PHP string.Results on the real code:
$nonceHashfrom its own base class.Tests
composer test: 321 tests and 851 assertions, against 307 and 824 onv3. New:CsrfTest,AjaxProviderTest, and twoMigrateToV3AccessTestcases that fail onv3's bones.csrf-live-smoke.sh:'csrf' => falseroute, the bench's menu page, and aloggedAjax action without$nonceHash.store()on the route and the menu page, and the Ajax action answers.Review
csrf($token)of the page's own was called;pages/forms were not checked.wpbones/wptablespackage runs bulk actions on GET, with no nonce. That is its own repository.composer test: 329 tests and 863 assertions.csrf-live-smoke.sh: 12 ✓.v3-access-live-smoke.sh: 18 ✓.