Mode d’emploi
- Copiez la commande et examinez le paquet avant de l’installer dans votre environnement agent.
- Consultez le document original pour les outils, identifiants et environnements requis.
- Décrivez les entrées et le livrable souhaité ; comparez la sortie aux sources.
npx skills add https://github.com/wpultimatesecurity/wordpress-security-skills --skill capability-permission-checksAvant de commencer
- L’installation n’inclut ni abonnement au modèle, ni quota API, ni accès aux fichiers.
- Étoiles et installations reflètent l’activité, pas un test indépendant de qualité ou de sécurité.
Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
Capability & permission checks (authorization)
When to use this skill
Use this skill wherever code does something not everyone should be able to do:
- Admin menu/submenu pages and settings screens.
- AJAX (
wp_ajax_*) and RESTpermission_callbacks. admin-post.phphandlers and action links (delete/approve/activate).- Editing options, users, posts, taxonomies, files, or plugin state.
- Anything reading or writing another user's data.
Authorization answers "is this user allowed?" — distinct from the nonce, which answers "did this request come from us?" (see nonces-csrf-protection). Use both.
Core principles (and why they matter)
- Check capabilities, not roles. Roles are bundles of capabilities that admins can
reassign. current_user_can( 'administrator' ) is a bug — test the capability (manage_options), so custom roles and multisite behave correctly.
- Use the most specific capability.
manage_optionsfor settings,edit_poststo
create posts, edit_post/delete_post (with an ID) for a specific post. Broad caps over-grant.
- Authorization and CSRF are independent. A capability check without a nonce is
CSRF-able; a nonce without a capability check lets any logged-in user act. Pair them.
- Re-check at every entry point. Hiding a menu item or button is UX, not security.
The handler that does the work must check again — the URL/endpoint is directly reachable.
- Per-object checks need the object ID.
current_user_can( 'edit_post', $post_id )
respects ownership and post-type rules; current_user_can( 'edit_posts' ) alone does not.
- Fail closed. Deny by default; only proceed when the check explicitly passes.
Step-by-step implementation
- Identify the operation and the capability it requires (use a meta cap with an ID
for per-object actions).
- At the start of the handler, after the nonce check:
if ( ! current_user_can( $cap[, $object_id] ) ) { deny(); }.
deny()=wp_die( ..., 403 ),wp_send_json_error( ..., 403 ), or a REST
WP_Error with rest_forbidden / status 403.
- For menus/REST, supply the capability where the API expects it
(add_menu_page capability arg, REST permission_callback).
- Never gate solely by hiding UI.
Supporting references
| Reference | Load when |
|---|---|
| Capability check examples | Implementing capability-gated admin pages, AJAX handlers, per-object actions, and REST callbacks. |
| Capability / authorization checklist | Before final verification of the capability / authorization controls. |
Common AI mistakes / anti-patterns
Mistake 1 — Checking the role instead of the capability
// ❌ Insecure: roles are mutable; this misses custom/multisite setups.
if ( in_array( 'administrator', wp_get_current_user()->roles, true ) ) {
do_admin_thing();
}// ✅ Secure: test the capability.
if ( current_user_can( 'manage_options' ) ) {
do_admin_thing();
}Mistake 2 — Relying on a hidden menu/button as the boundary
// ❌ Insecure: handler trusts that only admins saw the link.
add_action( 'admin_post_purge_cache', function () {
purge_cache(); // directly reachable via admin-post.php by anyone logged in
} );// ✅ Secure: the handler re-checks nonce + capability.
add_action( 'admin_post_purge_cache', function () {
check_admin_referer( 'purge_cache' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Forbidden', 'my-plugin' ), 403 );
}
purge_cache();
} );Mistake 3 — Generic capability where a per-object check is needed
// ❌ Insecure: any author can edit ANY post, not just their own.
if ( current_user_can( 'edit_posts' ) ) {
wp_update_post( array( 'ID' => $post_id, 'post_title' => $title ) );
}// ✅ Secure: meta capability with the object ID enforces ownership.
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( esc_html__( 'You cannot edit this post.', 'my-plugin' ), 403 );
}
wp_update_post( array( 'ID' => $post_id, 'post_title' => $title ) );Mistake 4 — REST route with no real permission callback
// ❌ Insecure: open to the world.
register_rest_route( 'my/v1', '/settings', array(
'methods' => 'POST',
'callback' => 'my_save',
'permission_callback' => '__return_true',
) );// ✅ Secure: enforce the capability in permission_callback.
register_rest_route( 'my/v1', '/settings', array(
'methods' => 'POST',
'callback' => 'my_save',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
) );Mistake 5 — Checking capability but never branching
// ❌ Insecure: result ignored; action runs regardless.
current_user_can( 'delete_users' );
delete_user_account( $id );// ✅ Secure: branch and fail closed.
if ( ! current_user_can( 'delete_users' ) ) {
wp_die( esc_html__( 'Forbidden', 'my-plugin' ), 403 );
}
delete_user_account( $id );Mistake 6 — Assigning roles or capabilities from request data
// ❌ Insecure: privilege escalation by posting role=administrator.
$user->set_role( $_POST['role'] );// ✅ Secure: validate the role against a hardcoded allowlist.
$allowed_roles = array( 'subscriber', 'contributor', 'author' );
$role = isset( $_POST['role'] ) ? sanitize_key( wp_unslash( $_POST['role'] ) ) : '';
if ( ! in_array( $role, $allowed_roles, true ) ) {
wp_die( esc_html__( 'Invalid role.', 'my-plugin' ), 400 );
}
$user->set_role( $role );Mistake 7 — Directly adding capabilities via user meta
// ❌ Insecure: grants arbitrary capabilities.
$user->add_cap( $_POST['capability'] );// ✅ Secure: never add capabilities from input; use role-based allowlists.
$allowed_caps = array( 'edit_posts', 'publish_posts' );
$cap = isset( $_POST['capability'] ) ? sanitize_key( wp_unslash( $_POST['capability'] ) ) : '';
if ( ! in_array( $cap, $allowed_caps, true ) ) {
wp_die( esc_html__( 'Invalid capability.', 'my-plugin' ), 400 );
}
$user->add_cap( $cap );Related: see the multisite-security skill for network vs. site scope and the nonces-csrf-protection skill for pairing these checks with a nonce.
Correct code examples
A complete example covering an admin page, an AJAX handler, a per-object action, and a REST permission callback is in `references/capability-check-example.php`.
// Common capabilities and when to use them.
current_user_can( 'manage_options' ); // site settings / options
current_user_can( 'edit_posts' ); // create/edit posts (general)
current_user_can( 'edit_post', $post_id ); // edit THIS post (ownership-aware)
current_user_can( 'publish_posts' ); // publish
current_user_can( 'upload_files' ); // media uploads
current_user_can( 'edit_users' ); // manage other users
current_user_can( 'install_plugins' ); // install/activate pluginsChecklist
- [ ] Every privileged handler calls
current_user_can()and branches on the result. - [ ] Capabilities are tested, never roles.
- [ ] The most specific capability is used; per-object actions pass the object ID.
- [ ] The check is paired with a nonce verification on state-changing requests.
- [ ] Admin pages re-check the capability on render (not just at menu registration).
- [ ] REST routes define a real
permission_callback(never__return_truefor writes). - [ ] AJAX handlers check capability after
check_ajax_referer. - [ ] Denied requests fail closed with a 403 /
WP_Error.




