In WordPress, nonces are often treated as universal protection for a form or AJAX request. This is a dangerous misconception: a nonce indicates that a request was likely generated in the correct context, but it does not prove that the user is authorized to perform the action.
Reliable validation consists of several layers at a minimum: user authentication, nonce verification, capability checks, and data validation and sanitization. Only after all these checks pass should code modify settings, posts, profiles, or other objects.
What Is a Nonce and What Does It Protect Against?
A WordPress nonce is a short-lived token associated with a user and an action context. Its purpose is to reduce the risk of a CSRF attack, in which an external website attempts to make an already authenticated user perform an unwanted request in WordPress.
For example, you can create a nonce for the update_profile action, add it to a form, or pass it to JavaScript, and then verify it during processing with check_admin_referer() or check_ajax_referer(). The action name should be specific so that the token is not unnecessarily reused for other operations.
However, a nonce is not a password, does not replace user authentication, and does not determine a user's role or permissions. If a nonce is exposed, or if the request is made by a user who already has access to the page, nonce verification alone will not prevent an unauthorized change.
- A nonce primarily protects against CSRF.
- A nonce does not grant permission to edit data.
- A nonce does not replace authentication or authorization.
- A nonce should not be treated as a secret intended for long-term storage or transmission between different actions.
Capabilities Define What a User Can Do
Capabilities are WordPress permissions that describe access to specific operations. Examples include manage_options, edit_posts, publish_posts, delete_posts, edit_users, and edit_user. You should check the capability that corresponds to the specific action rather than checking a role name.
Checking current_user_can('administrator') is the wrong approach. administrator is a role name, not a capability. In addition, role checks scale poorly: a site may have custom roles, operate in a multisite environment, or include users with a precisely defined set of permissions.
For objects, it is important to pass the correct identifier. For example, editing a post requires checking edit_post with the post ID, while changing a user requires the relevant permission together with the user ID. This allows WordPress to consider not only the user's general role, but also the specific object and its author.
A capability should provide the minimum access required. If an action changes one plugin option, there is no need to require excessively broad permissions such as manage_options when the plugin architecture allows you to create or use a separate capability.
- For access to a specific post, use an object-specific capability such as edit_post.
- Do not check roles by using names such as administrator, editor, or other strings.
- Do not rely on hiding a button in the interface: server-side validation is mandatory.
- Apply the principle of least privilege: users should receive only the permissions they need.
The Correct Order of Checks in a Request
For AJAX, admin-post.php, and custom REST handlers, checks should be performed in a consistent sequence. First verify the nonce and confirm that the required authentication is available, then check the capability, and only after that retrieve and validate the data. Modify the database only after all checks have succeeded.
It is best to verify the nonce before performing complex request processing. Capabilities must be checked on the server even if the relevant button is visible only to administrators. A user can manually construct a POST request and bypass any interface restriction.
The following is a simplified example for an AJAX action. The specific capability depends on whose information the handler modifies and which access rules your plugin requires.
- Verify the nonce for the specific action.
- Make sure the user is authenticated if the action is not intended to be available to guests.
- Check the capability using the correct object ID.
- Remove slashes from data received through POST or GET.
- Sanitize, validate, and normalize values.
- Perform the write operation only after all checks have passed.
check_ajax_referer( 'update_profile', 'nonce' );
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( 'Потрібна авторизація', 401 );
}
if ( ! current_user_can( 'edit_user', $user_id ) ) {
wp_send_json_error( 'Недостатньо прав', 403 );
}
$email = sanitize_email(
wp_unslash( $_POST['email'] ?? '' )
);
if ( ! is_email( $email ) ) {
wp_send_json_error( 'Некоректний email', 400 );
}
// Виконуйте зміну лише після всіх перевірок.
How to Validate Data After the Nonce and Capability Checks
A valid nonce does not make input data trustworthy. A user may submit an invalid email address, an unexpected number, a prohibited status value, or the ID of someone else's object. Each field must therefore be processed according to its type and purpose.
Use appropriate sanitization for text, but do not confuse sanitization with validation. sanitize_email() cleans a value, while is_email() checks whether it resembles a valid email address. For a numeric ID, you can use absint(), but that is not enough: after obtaining the ID, you must still verify that the object exists and that the user has access to that specific object.
For lists of permitted values, a whitelist is useful. For example, a status should be accepted only if it matches one of a predefined set of values. Do not pass unprocessed values into SQL or assume that a form field already has the correct format.
In WordPress, values from $_POST and $_GET often need to be processed with wp_unslash() because the environment may add slashes. After that, apply the required sanitization or validation.
- Define the expected type of every field.
- Use sanitization to clean values and validation to check whether they are acceptable.
- Check numeric ranges, string lengths, and lists of permitted values.
- For IDs, check not only the format but also access to the corresponding object.
- Do not output unsanitized data in HTML, JavaScript, or SQL.
$post_id = absint( $_POST['post_id'] ?? 0 );
$status = sanitize_key( wp_unslash( $_POST['status'] ?? '' ) );
if ( ! $post_id || ! get_post( $post_id ) ) {
wp_send_json_error( 'Об’єкт не знайдено', 404 );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( 'Недостатньо прав', 403 );
}
$allowed_statuses = array( 'draft', 'publish' );
if ( ! in_array( $status, $allowed_statuses, true ) ) {
wp_send_json_error( 'Недозволене значення', 400 );
}
AJAX, admin-post, and REST API Considerations
WordPress AJAX requests commonly use check_ajax_referer(). It can terminate execution with an error when the nonce is invalid, so subsequent code must not run. It is also important to separate handlers for authenticated and unauthenticated users: the existence of an AJAX route does not by itself mean that guests should have access to the action.
For forms submitted through admin-post.php, use check_admin_referer() with an action name. The handler must also check the capability, because a nonce only confirms the form context.
In the REST API, a nonce is often used for requests from the administration area, but a REST endpoint must have its own permission_callback. This callback should determine whether the current user is allowed to perform the operation. Do not leave permission_callback as a formality or make it always return true when the endpoint modifies data.
- AJAX: check the nonce and capability in the action callback.
- admin-post.php: use check_admin_referer() together with a separate permission check.
- REST API: implement access control in permission_callback.
- Do not treat a URL or action name as a secret security mechanism.
- Return appropriate errors and HTTP status codes without exposing unnecessary internal details.
add_action( 'admin_post_update_setting', 'my_update_setting' );
function my_update_setting() {
check_admin_referer( 'update_setting' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Недостатньо прав', 403 );
}
$value = sanitize_text_field(
wp_unslash( $_POST['value'] ?? '' )
);
// Перевірка допустимих значень і збереження налаштування.
}
Common Mistakes and How to Fix Them
The most common mistake is checking only the nonce. This code protects against some CSRF scenarios, but it does not prevent an authorized user from modifying someone else's post if the capability is not checked against the specific ID.
Another problem is checking the capability only while rendering the form. A button may be hidden, but the request can still be sent manually. Every access rule must be enforced in the server-side handler.
It is also dangerous to use one general nonce action for many different operations or to accept an object ID from the client without checking access rights. A value supplied by the user is not proof that the user owns the object.
Excessively broad permissions create another risk. If changing one post requires manage_options or an administrator check, a coding error could expose access to far more data than necessary.
- Mistake: treating a nonce as authorization. Solution: add current_user_can().
- Mistake: checking a role instead of a capability. Solution: check the specific permission.
- Mistake: controlling access only in the interface. Solution: repeat the check on the server.
- Mistake: trusting an ID from the request. Solution: verify that the object exists and that the user can access it.
- Mistake: saving data without validation. Solution: define the format and permitted values for every field.
Practical Pre-Release Checklist
Before publishing a plugin or theme, test every action under more than an administrator account. Test scenarios involving an unauthenticated visitor, a regular user, a user with a restricted role, and a user attempting to work with someone else's object.
Also test repeated submissions, ID substitution, missing fields, empty values, incorrect data types, and invalid nonces. Security must work regardless of whether the request comes from your JavaScript, a browser form, or a manually constructed HTTP client.
- Each action has its own nonce action.
- The nonce is checked in the server-side handler.
- The capability is checked immediately before the operation.
- The correct ID and object-specific capability are used for objects.
- All external data passes through unslash, sanitization, and validation.
- Guests cannot access private operations unless that behavior is deliberately implemented.
- Errors do not reveal passwords, tokens, SQL queries, or the system's internal structure.
- Access has been tested with different roles and against other users' objects.
Frequently asked questions
Is a nonce enough to secure a WordPress form?
No. A nonce primarily protects against CSRF, but it does not determine a user's permissions. Actions that modify data also require authentication, capability checks, and input validation.
Can I check the administrator role instead of a capability?
It is not recommended. Roles may differ between sites, and a role check does not describe the specific action well. Use current_user_can() with the required capability, such as edit_post, delete_post, or manage_options.
Where should a capability be checked—in JavaScript or PHP?
It must be checked in PHP on the server. JavaScript can hide a button or avoid displaying a form, but a user can easily bypass that restriction by sending the request manually.
Do I need to check the capability if the nonce was created for a specific user?
Yes. A nonce is associated with a user and context, but it is not permission to perform an operation. An authenticated user may have a nonce while still lacking permission to modify a specific post or setting.
Conclusion
Nonces and capabilities solve different problems: a nonce helps confirm the legitimacy of the request context, while a capability determines whether the user is authorized to perform the action. Reliable code never substitutes one for the other.
For every operation, check the nonce, access rights, and data on the server. This approach protects not only the form or AJAX request, but also the actual data-changing operation in WordPress.