WordPress Plugin Security: Nonces, Sanitization, Escaping & Capability Checks

Most WordPress security problems don’t come from WordPress itself — they come from plugins. A form that saves data without checking who submitted it, an AJAX handler that trusts user input, an admin page that any logged-in user can access. These are the patterns that get sites compromised.
This post covers the four security mechanisms every plugin developer needs to use consistently: nonces, sanitization, escaping, and capability checks. Each one closes a specific attack surface. Skip any one of them and you leave a gap.
Capability Checks: Who Can Do This?
The first question your plugin should ask on any sensitive action is whether the current user is allowed to perform it. WordPress handles this with current_user_can(), which checks against the built-in role and capability system.
// Basic capability check before processing anything
function my_plugin_save_settings(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to do this.' );
}
// safe to proceed
}
manage_options is the capability tied to the Administrator role. Here are the ones you’ll use most often:
| Capability | Who has it | Use when |
|---|---|---|
manage_options |
Administrator | Plugin settings, site-wide config |
edit_posts |
Editor, Author, Contributor | Creating or editing content |
publish_posts |
Editor, Author | Publishing content |
edit_others_posts |
Editor | Editing other users’ content |
manage_categories |
Editor, Administrator | Category/taxonomy management |
upload_files |
Author and above | Media uploads |
Always check the minimum capability needed for the action — don’t default to manage_options for everything just because it’s the most restrictive. If an action is relevant to editors, check edit_posts instead.
For REST API endpoints and AJAX handlers, the same rule applies:
// In a REST API callback
add_action( 'rest_api_init', function() {
register_rest_route( 'myplugin/v1', '/settings', [
'methods' => 'POST',
'callback' => 'my_plugin_update_settings',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
] );
} );
// In an AJAX handler — check capability before doing anything else
add_action( 'wp_ajax_my_plugin_action', function() {
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions', 403 );
}
// proceed
} );
Nonces: Verifying the Request Is Legitimate
A capability check confirms who the user is. A nonce confirms that the request actually came from your form — not from a malicious third-party site that tricked the user into submitting something. This is how you prevent CSRF (Cross-Site Request Forgery) attacks.
The name “nonce” is slightly misleading in WordPress — unlike a true cryptographic nonce, WordPress nonces can be reused within their validity window (24 hours by default). Think of them as short-lived tokens that tie a specific action to a specific user session.
Nonces in Admin Forms
// Step 1: Output the nonce field in your form
function my_plugin_settings_form(): void {
?>
<form method="post" action="options.php">
<input type="submit" value="Save Settings">
form>
>
```php functionmy_plugin_render_settings_page():void { $options =get_option('my_plugin_options',[]); $api_key = $options['api_key'] ??''; $webhook = $options['webhook_url'] ??''; $enabled =!empty($options['enabled']); ?> <divclass="wrap"> <h1>h1> <formmethod="post"> <tableclass="form-table"> <tbody><tr> <th><labelfor="api_key">label>th> <td> <inputtype="text"id="api_key"name="api_key"value="$api_key ); ?>"class="regular-text"> td> tr> <tr> <th><labelfor="webhook">label>th> <td> <inputtype="url"id="webhook"name="webhook_url"value="$webhook ); ?>"class="regular-text"> td> tr> <tr> <th>th> <td> <inputtype="checkbox"name="enabled"value="1"php=""checked(=""$enabled="");=""?=""> > td> tr> tbody>table> form> div> ``` PHP