Book a call

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

March 31, 2026·3 min read
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