WordPress Capabilities and Custom Roles: Secure Permission Management

WordPress stores user roles and capabilities in the wp_usermeta table under the key wp_capabilities. Misunderstanding how caps are checked lets attackers escalate privileges — so knowing the exact API is essential for any plugin that grants or checks permissions.

Problem: WordPress ships with a fixed set of roles, and permission checks scattered through plugins often check role names ($user->roles[0] === 'editor') rather than specific capabilities — making it fragile when roles are customised.

Solution: Use add_role() to create custom roles with a precise capability set, get_role()->add_cap() to extend existing roles, and always gate features with current_user_can('specific_cap') — never by role name. Register custom capabilities in a plugin activation hook so they are set once and persist.

The examples below create a custom role with a safe capability set, add individual caps to existing users, and show the correct way to check capabilities in REST API endpoints.

// Register a custom role on plugin activation (run once via register_activation_hook)
function myplugin_create_roles() {
    add_role(
        'content_manager',
        __( 'Content Manager', 'myplugin' ),
        [
            'read'                   => true,
            'edit_posts'             => true,
            'edit_others_posts'      => true,
            'edit_published_posts'   => true,
            'publish_posts'          => true,
            'delete_posts'           => false, // explicitly deny
            'upload_files'           => true,
            'manage_categories'      => true,
        ]
    );
}
register_activation_hook( __FILE__, 'myplugin_create_roles' );

// Remove the role on deactivation
register_deactivation_hook( __FILE__, function() {
    remove_role( 'content_manager' );
} );

Grant or revoke individual capabilities on a per-user basis without changing their role:

// Add a specific cap to a user
$user = get_user_by( 'login', 'jane' );
$user->add_cap( 'manage_woocommerce' );  // stored in wp_usermeta

// Remove a cap
$user->remove_cap( 'manage_woocommerce' );

// Check capability — ALWAYS use current_user_can(), never inspect the meta directly
if ( current_user_can( 'manage_woocommerce' ) ) {
    // safe to proceed
}

// Protect a REST endpoint
add_action( 'rest_api_init', function() {
    register_rest_route( 'myplugin/v1', '/report', [
        'methods'             => 'GET',
        'callback'            => 'myplugin_report_handler',
        'permission_callback' => function() {
            // Never use __return_true() in production
            return current_user_can( 'manage_options' );
        },
    ] );
} );

// DANGER: Never store raw capability names from user input
// BAD:  $user->add_cap( sanitize_text_field( $_POST['cap'] ) );
// GOOD: Use an allowlist
$allowed_caps = [ 'edit_posts', 'upload_files' ];
$requested    = sanitize_text_field( $_POST['cap'] ?? '' );
if ( in_array( $requested, $allowed_caps, true ) ) {
    $user->add_cap( $requested );
}

NOTE: Never rely on role names for permission checks — always check the specific capability. Roles can be modified by other plugins; current_user_can( 'edit_posts' ) is always safer than $user->roles[0] === 'editor'.

Leave Comment

Your email address will not be published. Required fields are marked *