A CSRF attack tricks an authenticated user into performing an action they did not intend to perform, such as changing settings, creating a post, updating a profile, or deleting data. The browser may automatically attach WordPress cookies to the request, so the server treats it as a request from a legitimate user.
In WordPress, the primary defense against this scenario is a nonce: a special token associated with a specific action, user, and session. Nonces are needed in custom plugins, Elementor widgets, administrative forms, AJAX handlers, and custom REST requests.
What Is CSRF and Why Is It Dangerous for WordPress?
CSRF, or Cross-Site Request Forgery, occurs when a third-party website or malicious script causes a user's browser to send a request to your WordPress site. If the user is authenticated, cookies may be attached automatically.
For example, an attacker may prepare a form that sends a POST request to a settings-change handler. The user opens the page, clicks a button, or is taken to it through automatic form submission, and WordPress sees a valid session.
The risk is especially high for requests that change the system's state: creating or deleting posts, changing an email address or password, updating settings, or modifying orders or profile data. Simply reading public data is not usually a typical CSRF target, but every data-changing operation should be protected.
- CSRF takes advantage of the user's existing authentication.
- Checking only for the presence of cookies is not sufficient.
- Every individual action must be protected, not just the page as a whole.
How a WordPress Nonce Protects a Form
A WordPress nonce is a token that indicates that a request was most likely generated by your interface for a specific action. The token is associated with an action string, user, and time window, so a random third-party site should not know its value.
The name nonce comes from the English phrase “number used once,” but in WordPress it is not a strictly one-time cryptographic token. It remains valid for a limited period and may be reused during that period. Therefore, a nonce must not be treated as a replacement for a one-time code, password, or authentication mechanism.
Use a separate, descriptive action for each operation, such as save_profile, delete_invoice, or update_widget_settings. Avoid using one generic nonce for all administrative actions.
- A nonce protects against request forgery, but it does not determine whether the user is authorized to perform the action.
- The action should describe one specific operation.
- A missing, expired, or incorrect nonce must cause the request to be rejected.
<?php
wp_nonce_field( 'save_profile', 'profile_nonce' );
?>
Adding a Nonce to a Standard Form
The wp_nonce_field() function creates a hidden field containing the nonce and, if needed, an additional field for referrer validation. Place it inside the form that performs the data-changing operation.
<?php if ( current_user_can( 'edit_user', get_current_user_id() ) ) : ?>
<form method="post">
<?php wp_nonce_field( 'save_profile', 'profile_nonce' ); ?>
<input type="email" name="email" required>
<button type="submit">Save</button>
</form>
<?php endif; ?>
Verifying the Nonce and User Capabilities on the Server
A hidden field by itself provides no protection. The server-side handler must receive the token, verify it before performing a dangerous operation, and then separately check the current user's capabilities.
Administrative forms commonly use check_admin_referer(). It verifies the nonce and may terminate execution with an error if the token is invalid. Front-end handlers can use wp_verify_nonce() with explicit result handling. This makes it possible to return a controlled message to the user instead of terminating unexpectedly.
Checking capabilities with current_user_can() is mandatory. Even a valid nonce does not mean that the user is allowed to edit a particular profile, post, or setting.
- First check the request method and nonce.
- Then check the user's capabilities.
- Do not accept an object ID without verifying that the user is authorized to work with that specific object.
- Sanitize and validate input only after the preliminary checks.
<?php
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
wp_die( 'Invalid request method.' );
}
check_admin_referer( 'save_profile', 'profile_nonce' );
if ( ! current_user_can( 'edit_user', get_current_user_id() ) ) {
wp_die( 'Insufficient permissions.' );
}
$email = isset( $_POST['email'] )
? sanitize_email( wp_unslash( $_POST['email'] ) )
: '';
if ( ! is_email( $email ) ) {
wp_die( 'Invalid email address.' );
}
// Perform the permitted operation here, after all checks have passed.
?>
Nonces in WordPress AJAX Requests
AJAX handlers often become a source of vulnerabilities because the logic is moved into a separate function and the developer adds security checks later. For these requests, send the nonce together with the data and verify it with check_ajax_referer().
A nonce can be sent in the _ajax_nonce parameter or another consistently named parameter. Do not generate it manually on the client. The server should create it with wp_create_nonce(), and JavaScript should receive it through localized data or another secure mechanism provided by WordPress.
- Use different actions for different AJAX operations.
- Do not treat the fact that a request is sent via AJAX as evidence that it is legitimate.
- After verifying the nonce, still check permissions and the operation's parameters.
- For unauthenticated users, explicitly determine whether they really need access to the handler.
<?php
wp_localize_script(
'profile-script',
'ProfileData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'save_profile_ajax' ),
)
);
function save_profile_ajax() {
check_ajax_referer( 'save_profile_ajax', 'nonce' );
if ( ! is_user_logged_in() || ! current_user_can( 'edit_user', get_current_user_id() ) ) {
wp_send_json_error( array( 'message' => 'Insufficient permissions.' ), 403 );
}
$email = isset( $_POST['email'] )
? sanitize_email( wp_unslash( $_POST['email'] ) )
: '';
if ( ! is_email( $email ) ) {
wp_send_json_error( array( 'message' => 'Invalid data.' ), 400 );
}
// Data-changing operation.
wp_send_json_success();
}
add_action( 'wp_ajax_save_profile', 'save_profile_ajax' );
?>
Specific Considerations for Elementor and Custom Widgets
A standard Elementor form or third-party widget may have its own processing mechanisms, but that does not mean a custom action is automatically protected. If your code receives data through a hook, AJAX request, or custom endpoint and changes WordPress's state, the nonce must be added and verified in your own logic.
In a custom widget, do not rely on a hidden field, button name, or HTML structure. An attacker can construct a request without using the browser form. Security must be implemented on the server, regardless of whether the request came from Elementor, a standard template, or an external client.
If a widget only sends a message by email without changing authenticated data, the threat model may be different. However, input validation, rate limiting, spam protection, and proper handling of personal data are still required.
- Verify the nonce in the PHP handler that actually performs the action.
- Do not treat Elementor or another page builder as a replacement for server-side authorization.
- Check that the submitted post, order, or profile belongs to the current user when applicable.
- Do not expose unnecessary internal data or PHP error text in responses.
REST API, Referer, and Additional Protection Layers
For custom WordPress REST routes, define a permission_callback and check permissions there. When cookie-based authentication is used from an administrative interface, use a REST nonce, usually sent in the X-WP-Nonce header. The header itself does not replace permission checks.
The HTTP Referer header can be a useful additional signal, but it must not be the only defense. It may be absent because of privacy settings, browser policies, or network conditions. Likewise, do not rely solely on hidden form fields: they are easy to forge.
To reduce risk, also use HTTPS, appropriate Secure and HttpOnly cookie attributes, a reasonable SameSite policy, Origin validation where appropriate, and rate limiting for sensitive operations. These measures complement nonces but do not replace them.
- A REST route without permission_callback must not perform protected operations.
- Do not use a nonce as an API key or long-term secret.
- Do not log nonces or expose them in public responses or third-party analytics systems unless necessary.
<?php
register_rest_route(
'site/v1',
'/profile',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => 'site_update_profile',
'permission_callback' => function () {
return is_user_logged_in()
&& current_user_can( 'edit_user', get_current_user_id() );
},
)
);
?>
Common Mistakes When Protecting Forms
The most common mistake is adding a nonce to the form but failing to verify it in the handler. In that case, the token is merely decorative and has no effect on security.
Another problem is verifying the nonce while skipping authentication and access control. A user with a valid token must not be allowed to change someone else's data simply because the request was generated from your interface.
It is also dangerous to use one nonce for every operation, trust client-supplied parameters, skip request-method checks, or confuse CSRF protection with XSS protection. A nonce does not sanitize HTML, clean SQL queries, or protect against session theft.
- A nonce is present, but the action used during generation differs from the one used during verification.
- The token is verified after the dangerous operation has already been performed.
- The user's role or authentication status is checked, but not the capability required for the specific action.
- Data from $_POST or $_GET is used without wp_unslash(), sanitization, and validation.
- A failed verification is ignored so that the form “works” under all conditions.
- Protection is present in the HTML form but missing from the AJAX or REST handler.
Frequently asked questions
Is a nonce enough to protect a WordPress form?
No. A nonce protects against request forgery, but it does not replace authentication or authorization checks. You must also check current_user_can(), validate the data, verify object ownership, and handle the result securely.
Should a nonce be added to every form?
A nonce is needed for forms and requests that change the system's state or perform a sensitive action. Each logical operation should preferably use its own action rather than one shared token for the entire site.
What is the difference between check_admin_referer() and check_ajax_referer()?
Both functions verify a nonce, but they are intended for different scenarios. check_admin_referer() is generally used for administrative forms, while check_ajax_referer() is used for AJAX requests. The choice of function does not eliminate the need for separate permission checks.
Does a nonce protect against XSS or SQL injection?
No. A nonce is specifically intended to defend against CSRF. XSS protection requires escaping output and handling HTML safely, while protection against SQL injection requires prepared queries and proper data validation.
Why does a nonce sometimes become invalid after a page has been open for a long time?
A nonce has a limited validity window and may depend on the current session. If the page remains open for a long time or the session changes, the token may be rejected. In that case, refresh the form or provide an appropriate message to the user.
Conclusion
Protecting a WordPress form against CSRF is not a matter of adding one hidden field. It requires a sequence of checks: a nonce for the specific action, authentication, capability verification, object-level access control, data validation, and safe execution of the operation.
For custom plugins, Elementor widgets, AJAX, and REST routes, verify protection on the server and test requests without the standard form. If the handler rejects requests without a nonce, prevents unauthorized actions, and does not trust browser-supplied data, the CSRF risk is significantly reduced.