Overview: why nonces and capabilities matter for WordPress forms
WordPress nonces and capability checks protect your application from common web attacks and privilege escalation:
- Nonces (number used once) mitigate Cross-Site Request Forgery (CSRF) by ensuring a request originates from a trusted user and page.
- Capabilities (current_user_can, user_can) ensure the current user has the right privileges to perform the requested action (authorization).
Never rely on nonces alone for authorization. Nonces protect intent and origin capability checks confirm permissions. Combine them, always sanitize inputs, and escape outputs.
Nonces: what they are and how they work
A WordPress nonce is a keyed token generated for a specific user and action. Nonces are time-limited (default lifetime configurable via the nonce_life filter WordPress default is 24 hours), and are not persistent tokens stored in the database. They are suitable for preventing CSRF in state-changing requests (POST, DELETE, etc.). They are not proper authentication tokens and should not be used as the only protection.
Important properties
- Action-scoped: you generate nonces for a specific action string verification checks that action.
- User-scoped: nonces are tied to the current user/session.
- Time-limited: default lifetime is 24 hours (filterable with nonce_life).
- Stateless: WordPress does not persist each nonce it derives them from user/session/time and a secret.
Core functions — generation and verification
- wp_create_nonce(action) — create a nonce string for an action (useful for JavaScript or URLs).
- wp_nonce_field(action, name, referer, echo) — output a hidden input with the nonce and optionally a referer field (helper for forms).
- wp_nonce_url(url, action, name) — append a nonce to a URL querystring (useful for links that perform actions).
- wp_verify_nonce(nonce, action) — low-level verification returns 1 or 2 (valid in current or previous tick) or false.
- check_admin_referer(action, query_arg) — verifies nonce and referer for admin-posted forms (dies on failure unless you pass false to the third arg).
- check_ajax_referer(action, query_arg, die) — verifies a nonce used for AJAX requests (often used in admin-ajax.php handlers).
General best practices
- Only use nonces for actions that change state (create, update, delete) they are not needed for read-only operations.
- Always check a user capability with current_user_can() for authorization after verifying the nonce.
- Sanitize all incoming data (sanitize_text_field, sanitize_email, wp_kses_post, etc.).
- Escape all output (esc_attr, esc_html, esc_url) to avoid XSS.
- Use POST for state-changing operations prefer not to place critical actions in GET links unless you also validate capabilities and nonces.
- For AJAX in JavaScript, pass the nonce via a JS object using wp_localize_script or use the X-WP-Nonce header for REST requests.
- For unauthenticated users, nonces are less reliable consider server-persisted CSRF tokens or reCAPTCHA for anonymous forms.
Examples
The examples below show common patterns: an admin form (server-side processing), a front-end form (shortcode), an admin-ajax AJAX handler (JS PHP), and a REST route. Every code block is wrapped in the specified Enlighter pre tag with the correct language.
1) Admin settings form (wp-admin) with wp_nonce_field and check_admin_referer
This example adds an options page with a form. The form includes a nonce via wp_nonce_field. The form handler verifies the nonce using check_admin_referer and checks capability with current_user_can(manage_options).
Settings saved.
}
// Display form
value = esc_attr( get_option(my_plugin_text, ) )
?>
Notes
- check_admin_referer both verifies the nonce (using wp_verify_nonce internally) and optionally the referer field it calls wp_die() by default on failure. To handle failures gracefully, call wp_verify_nonce directly and handle the result.
- Always check capabilities even for admin pages because a crafted POST request from a low-privilege user could otherwise be accepted by nonce-only checks (nonces are user-scoped but you still should ensure correct capability).
2) Front-end form via shortcode with explicit wp_create_nonce and handling in admin-post.php
A public form handled by admin-post.php (wp-admin/admin-post.php) supports logged-in and logged-out handlers. For logged-in users, nonces work well. For logged-out users, consider extra server-stored tokens or CAPTCHA.
3) AJAX example (admin-ajax.php): enqueue script, localize nonce, verify server-side
This shows a JS client making an AJAX POST, sending a nonce, and the PHP handler verifying the nonce with check_ajax_referer and checking capabilities.
admin_url(admin-ajax.php),
nonce => wp_create_nonce(my_ajax_action) // server-generated nonce for the action
))
})
// AJAX handler for logged-in users
add_action(wp_ajax_my_ajax_action_handler, my_ajax_action_handler)
// Optionally for logged-out users
add_action(wp_ajax_nopriv_my_ajax_action_handler, my_ajax_action_handler)
function my_ajax_action_handler() {
// Verify nonce (third arg true means die on failure)
check_ajax_referer(my_ajax_action, security)
// Capability check (only if action requires it)
if ( ! current_user_can(edit_posts) ) {
wp_send_json_error(array(message => Insufficient permissions), 403)
}
value = isset(_POST[value]) ? sanitize_text_field(_POST[value]) :
// Process...
wp_send_json_success(array(received => value))
}
?>
// my-ajax.js
jQuery(function(){
(#my-button).on(click, function(e){
e.preventDefault()
.post(MyAjaxData.ajax_url, {
action: my_ajax_action_handler,
security: MyAjaxData.nonce, // the nonce field name we check in PHP
value: (#some-input).val()
}, function(response){
if (response.success) {
console.log(Server replied:, response.data)
} else {
console.error(AJAX error, response)
}
})
})
})
Notes for AJAX
- Use check_ajax_referer to verify AJAX nonces. It checks POST (or GET) fields and the X-WP-Nonce header.
- Always check capabilities server-side too JavaScript is untrusted.
- For REST API endpoints, prefer using the REST-style nonce (X-WP-Nonce) or the REST permission_callback mechanism described below.
4) REST API endpoint: permission_callback and X-WP-Nonce
The REST API has built-in capability to use a nonce passed in the request header X-WP-Nonce. On the JS side, when using the REST client (wp-api or your own code), send the nonce with that header (common pattern is to localize the nonce or use wpApiSettings.nonce). On the PHP side, register your route and use a permission_callback to check capabilities. You can also verify the nonce manually with wp_verify_nonce on the header value if you need custom logic.
POST,
callback => myplugin_rest_save,
permission_callback => myplugin_rest_permissions_check
))
})
function myplugin_rest_permissions_check( request ) {
// 1) Optionally verify the X-WP-Nonce (header)
header_nonce =
if ( isset( _SERVER[HTTP_X_WP_NONCE] ) ) {
header_nonce = sanitize_text_field( wp_unslash( _SERVER[HTTP_X_WP_NONCE] ) )
}
// Verify the nonce created with wp_create_nonce(wp_rest)
if ( header_nonce ! wp_verify_nonce( header_nonce, wp_rest ) ) {
return new WP_Error( rest_nonce_invalid, Invalid nonce, array(status => 403) )
}
// 2) Capability check
if ( ! current_user_can( edit_posts ) ) {
return new WP_Error( rest_forbidden, You cannot perform this action, array(status => 403) )
}
return true
}
function myplugin_rest_save( request ) {
params = request->get_json_params()
value = isset(params[value]) ? sanitize_text_field(params[value]) :
// Process and return response
return rest_ensure_response(array(saved => value))
}
?>
Client-side example sending X-WP-Nonce (fetch)
// Suppose you localized MyRest with a nonce and root keys:
fetch(MyRest.root myplugin/v1/save, {
method: POST,
credentials: same-origin, // important to keep cookies auth
headers: {
Content-Type: application/json,
X-WP-Nonce: MyRest.nonce
},
body: JSON.stringify({ value: some data })
}).then(r => r.json()).then(console.log)
Using nonces in URLs (links that perform actions)
If you must include a nonce in a link (for example, to trigger a one-click action), generate it with wp_nonce_url or add wp_create_nonce and append a query arg named _wpnonce (or custom name). On the server verify it with check_admin_referer or wp_verify_nonce.
post_id), admin_url(admin.php?page=my_page) ), delete_post_ . post_id, _wpnonce )
// Then on handler:
if ( isset(_GET[_wpnonce]) wp_verify_nonce(_GET[_wpnonce], delete_post_ . post_id) ) {
// check capability
if ( current_user_can(delete_post, post_id) ) {
wp_delete_post(post_id)
}
}
?>
Edge cases and advanced considerations
- Nonce lifetime and timing: You can change the default lifetime via the nonce_life filter. Keep in mind wp_verify_nonce accepts previous tick as valid, so slightly more than the configured tick is allowed. If you change lifetime keep UX in mind.
- Nonces for anonymous users: Since nonces are tied to user/session, they are not a reliable CSRF defense for anonymous users. Use server-generated tokens persisted to session/transient or use CAPTCHA for spam-sensitive forms.
- Do not use nonces as the only authorization: Nonces prove intent, not permission. Always check capabilities for actions that require specific privileges.
- Use HTTPS for security-sensitive requests to protect nonces in transit.
- Avoid exposing raw database IDs in links without capability checks – always verify the user is allowed to act on the target resource (current_user_can(edit_post, post_id)).
- Manage graceful failure: check_admin_referer and check_ajax_referer will by default call wp_die for custom behavior call wp_verify_nonce and handle failures to return JSON errors or redirect with an error notice.
Putting it all together: checklist before deploying a form
- Is the form performing a state-changing action? If yes: use a nonce.
- Is the action privilege-restricted? Add a capability check (current_user_can).
- Are you sanitizing inputs and escaping outputs? (sanitize_, esc_)
- Are you using POST for state changes? (Avoid GET unless properly protected.)
- For AJAX: send the nonce as POST field or X-WP-Nonce header verify with check_ajax_referer or wp_verify_nonce.
- For REST: use permission_callback and either rely on core verification of X-WP-Nonce or verify the nonce manually and check capabilities.
- For anonymous submissions: consider server-persisted CSRF tokens, honeypots, or CAPTCHA.
Troubleshooting common issues
- Nonce verification fails: Confirm the nonce you generated and the action string used to verify are identical. Ensure you are sending the nonce in the expected field name. Remember nonces expire check the nonce_life or regenerate the nonce on page load.
- Ajax nonces not recognized: If you set the nonce in a JS object, ensure the localized object is available in the page where your script runs and that your AJAX sends the field name the PHP expects.
- REST calls failing with 403: Ensure you send the X-WP-Nonce header and that the nonce was generated via wp_create_nonce(wp_rest) or use appropriate REST auth (Application Passwords, OAuth, cookie authentication).
- Logged-out users can’t pass nonce checks: Consider alternative CSRF protection for anonymous forms (server-persisted token or CAPTCHA) — nonces are user/session scoped.
Further references
Summary
To protect WordPress forms, always combine nonces (for CSRF protection) with capability checks (for authorization), sanitize inputs, and escape outputs. Use wp_nonce_field or wp_create_nonce for generation, check_admin_referer / check_ajax_referer / wp_verify_nonce for verification, and employ REST permission_callback for REST endpoints. For anonymous interactions, use server-persisted CSRF tokens or other anti-spam measures. These practices significantly reduce the risk of CSRF and unauthorized actions in your plugin or theme.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂
|
¡Si te ha servido el artículo ayúdame compartiendolo en algún sitio! Pero si no te ha sido útil o tienes dudas déjame un comentario! 🙂
Related