SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Nonces and Capabilities

WordPress Plugin Development Lesson 12 of 27 ~3 min read

Overview

Every action that changes data in a plugin needs two checks: a capability check confirms the current user is allowed to do it, and a nonce confirms the request was made deliberately from your own form rather than forged by another site.

Two different attacks

CheckAnswers the questionStops
current_user_can()Is this user allowed to do this?A Subscriber deleting books by guessing the URL
NonceDid this user mean to do this, from our page?CSRF: an admin visiting an evil page that silently submits your form in their logged-in browser
Watch out: A nonce is not an authentication check. A logged-in Subscriber can get a valid nonce from a page they can see. You always need both: nonce and capability.

Protecting a form

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="gdc_books_reset_stats">
    <?php wp_nonce_field( 'gdc_books_reset_stats', 'gdc_books_nonce' ); ?>
    <?php submit_button( __( 'Reset statistics', 'gdc-books' ), 'delete' ); ?>
</form>

wp_nonce_field( $action, $name ) prints a hidden field containing a token tied to the action name, the current user and a time window of 12 to 24 hours.

Handling it with admin-post.php

<?php
function gdc_books_handle_reset_stats() {
    // 1. Capability.
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'You are not allowed to do this.', 'gdc-books' ), 403 );
    }

    // 2. Nonce. Dies with "The link you followed has expired" on failure.
    check_admin_referer( 'gdc_books_reset_stats', 'gdc_books_nonce' );

    // 3. The actual work.
    delete_transient( 'gdc_books_stats' );

    // 4. Redirect back (POST/redirect/GET), so a refresh does not resubmit.
    wp_safe_redirect( add_query_arg( 'gdc-reset', '1', wp_get_referer() ) );
    exit;
}
// admin_post_{action} — the value of the hidden "action" field.
add_action( 'admin_post_gdc_books_reset_stats', 'gdc_books_handle_reset_stats' );

admin-post.php is WordPress's front door for custom form submissions. It fires admin_post_{action} for logged-in users and admin_post_nopriv_{action} for visitors.

Protecting a link

An action triggered by a link — "Delete this book" — needs a nonce in the URL:

<?php
$url = wp_nonce_url(
    admin_url( 'admin-post.php?action=gdc_books_archive&book=' . $book_id ),
    'gdc_books_archive_' . $book_id // Include the ID: a nonce for book 5 cannot archive book 6.
);
echo '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Archive', 'gdc-books' ) . '</a>';

// In the handler:
$book_id = isset( $_GET['book'] ) ? absint( $_GET['book'] ) : 0;
check_admin_referer( 'gdc_books_archive_' . $book_id );
if ( ! current_user_can( 'edit_post', $book_id ) ) {
    wp_die( '', 403 );
}

Object capabilities

Capabilities such as edit_post take the post ID and answer for that specific post. An Author can edit their own books but not someone else's. Always pass the ID when the action concerns one object.

Checklist for every handler

  1. Check the capability, with the object ID if there is one.
  2. Verify the nonce: check_admin_referer(), check_ajax_referer() or wp_verify_nonce().
  3. Unslash, then sanitise and validate every input (next lesson).
  4. Do the work.
  5. Escape anything you output, or redirect.

Practice

  • Build the "Reset statistics" form and handler with admin-post.php.
  • Remove the nonce field and submit — read the error.
  • Log in as an Editor and try the handler URL; confirm the capability check stops you.
Create a free account to save which lessons you've finished. Save my progress