Nonces and Capability Checks Explained: A Practical Guide
Nonces and capability checks are not the same mechanism and they do not do the same job. A nonce proves the request came from the right form at the right time. A capability check proves the user is allowed to do the thing they're asking to do. Ship a plugin with only one of the two and you've built a security hole — this happens constantly in plugins on WordPress.org, and it's one of the fastest ways to get pulled from the directory or show up in a vulnerability database.
If you're building or maintaining a plugin, understanding exactly where each one applies — and where they overlap — is the difference between a clean security audit and a CVE with your plugin's name on it.
What a Nonce Actually Does
A nonce ("number used once") is a token WordPress generates to verify that a request originated from a legitimate page on your site, not from a forged link or a cross-site request forgery (CSRF) attack. It's tied to a specific action name, a time window, and in most cases the current user's session.
wp_create_nonce( 'my_action' )generates the tokenwp_nonce_field( 'my_action' )drops it into a form as a hidden inputwp_verify_nonce( $_POST['_wpnonce'], 'my_action' )checks it on submitcheck_admin_referer()andcheck_ajax_referer()wrap verification plus a die-on-failure for you
Nonces expire — by default after 24 hours (technically two 12-hour "ticks," which is why a nonce can fail right around the 24-hour mark even though it looks like it should still be valid). They are not a secret the way a password is. A nonce protects against a forged request being replayed from somewhere else; it says nothing about whether the person submitting it should be allowed to do so.
What Nonces Don't Protect Against
This is where most WordPress devs get tripped up. A nonce does not check permissions, does not sanitize input, and does not stop SQL injection or XSS. It only confirms the request came from a page your site generated for that user, recently. Pair it with insecure query building and you still have a vulnerable plugin — the nonce just means the attacker needs to get their forged request onto a page the logged-in victim visits, which is exactly how CSRF exploits against plugins like outdated form or caching tools have worked in the wild.
What a Capability Check Actually Does
Capability checks are the authorization layer. current_user_can( 'manage_options' ) or current_user_can( 'edit_posts' ) asks: does this specific logged-in user have permission to perform this action, regardless of how the request arrived?
WordPress capabilities are granular by design. Don't default to checking a role ("is this an administrator?") — check the capability that actually matches the action ("can this user manage options?"). Roles are just named bundles of capabilities, and site owners remap them constantly with plugins like Members or User Role Editor. If your plugin hardcodes a role check instead of a capability check, it breaks the moment a site uses custom roles.
| Mechanism | Question it answers | Typical function | Fails when |
|---|---|---|---|
| Nonce | Did this request come from my own form, recently? | wp_verify_nonce() | Token missing, expired, or action name mismatched |
| Capability check | Is this user allowed to do this? | current_user_can() | User lacks the capability, even with a valid nonce |
| Sanitization/escaping | Is this data safe to use/output? | sanitize_text_field(), esc_html() | Neither of the above — this is a separate layer entirely |
Nonces and Capability Checks Explained Together: Why You Need Both
Here's the sequence that matters in practice, every time you handle a form submission, AJAX call, or admin-post action:
- Verify the nonce first — reject fast if it's invalid, no need to touch the database
- Check the capability second — confirm the user is actually authorized
- Sanitize and validate the input third
- Then, and only then, perform the action
A real example from an admin-post handler:
if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'save_widget_settings' ) ) { wp_die( 'Security check failed' ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Insufficient permissions' ); }
Skip step 1 and you're open to CSRF. Skip step 2 and any logged-in user — including a Subscriber who got a valid nonce by viewing a page they shouldn't have access to — can trigger admin-level actions. This exact gap (valid nonce, missing capability check) is one of the most common root causes behind "privilege escalation" CVEs logged against WordPress plugins, especially in form builders, page builders, and settings-import features.
AJAX Handlers Need Both, Every Time
wp_ajax_ hooks are a common blind spot. check_ajax_referer() handles the nonce side, but it does not check capabilities — you have to add that yourself. If your AJAX endpoint exports site data, modifies settings, or deletes content, and you only call check_ajax_referer(), you've verified the request is genuine but not that the requester should be allowed to make it.
Where This Shows Up in Real Plugins
Settings pages, bulk actions, import/export tools, and anything behind admin-ajax.php are the highest-risk surfaces. Plugins that register custom post types with public REST endpoints need the same discipline — permission_callback in a REST route registration is your capability check; don't leave it as __return_true unless the endpoint is genuinely meant to be public.
Form plugins are a good case study because they handle untrusted input by design. Contact Form 7 and most entries in our form plugin comparison rely on nonces for submission integrity but deliberately skip capability checks on the public-facing submit action, since anonymous visitors are supposed to submit the form — the capability check matters on the admin side, where form settings and saved entries live. Commerce plugins like WooCommerce layer both checks heavily around order management and settings precisely because the attack surface (money, customer data) is higher value.
Why This Matters Beyond Avoiding a CVE
A security disclosure against your plugin doesn't stay contained to a changelog entry. It shows up in vulnerability feeds, gets picked up by security scanners that site owners run before installing anything, and directly affects whether your plugin keeps climbing in the WordPress plugin rankings. Install counts and active-user trends are public on wordpress.org, and a visible CVE tends to flatten growth right when you'd otherwise be gaining ground — see our breakdown of what healthy plugin growth actually looks like for how sensitive that curve is to trust events.
It also affects how you're perceived relative to competitors. If you're using our competitor analysis tools to benchmark against plugins in your category, security posture is part of what separates the plugins that sustain growth from the ones that spike and stall. You can also run a plugin and theme detector scan against competitor sites to see which security-relevant plugins (firewalls, backups) they're layering on top of their stack — it's a quick signal for how seriously a site owner in your niche takes this.
A Quick Audit Checklist
- Every state-changing form has a nonce field and a matching
wp_verify_nonce()orcheck_admin_referer()call - Every nonce verification is followed by an explicit
current_user_can()check — never assume the nonce implies permission - Capability checks use the specific capability (
edit_posts,manage_woocommerce) rather than a role name (administrator) - Every
wp_ajax_handler checks both, even "read-only" ones that expose data - Every REST route has a real
permission_callback, not__return_true, unless the endpoint is intentionally public - Backups are current before you ship a security patch — if the fix changes data handling, you want a rollback path; see our backup plugin comparisons if you don't already have one in your own stack
None of this replaces sanitization and output escaping — that's a third, separate layer. Nonces and capability checks only answer "is this request genuine" and "is this user allowed," respectively. You still need to treat every input as hostile once it clears both gates.
Frequently Asked Questions
WP Stats Team
The WP Stats team analyzes data from 69,000+ WordPress plugins and 28,000+ themes on WordPress.org. We publish practical, data-backed guidance on plugin SEO, growth, and competitive analysis for developers and marketers.
View all articles by WP Stats Team