Guide · Roles and capabilities

Administrator rights can be handed out without creating a single administrator.

Your administrator list looks right, yet a subscriber or customer account reached the Plugins screen, settings changed that nobody admits to, or a security plugin warns that a role has unexpected capabilities. WordPress decides what a user can do from two places in the database: the role definitions stored in the user_roles option, and each user’s own capabilities entry in usermeta. Change either and an account that the Users screen still calls “Subscriber” can install plugins, edit files and create administrators. This guide shows how to read both without changing anything, how to tell a plugin’s legitimate custom role from a tampered one, and how to put roles back without breaking WooCommerce or membership plugins.

Symptom
A low-privilege account can reach Settings, Plugins, Users or the file editors
Mechanism
Capabilities added to a role definition, or to one user’s usermeta
Why it hides
The Users screen shows the role name, not what the role can do
Restore with
WP-CLI role and capability commands, after preserving the originals

How WordPress decides

Two database entries decide what every account can do

The user_roles option holds a serialised array of every role on the site — administrator, editor, author, contributor and subscriber, plus any role a plugin added — and the capabilities each one grants. Change the subscriber entry to include manage_options or install_plugins and every subscriber on the site gains that right at once, while the Users screen goes on listing them as subscribers.

Each user also has a capabilities entry in usermeta: normally a one-entry serialised array naming their role. WordPress lets that entry carry individual capabilities alongside the role, so one account can be given manage_options or edit_users on its own. Again the Users screen shows only the role.

Either change needs a write to the database — through a vulnerable plugin, a stolen administrator login, or code already running on the site. It is a quiet form of persistence: the attacker keeps an ordinary-looking account and no new administrator ever appears.

Figure 1 · Admin rights without an admin

Two ways to hand out administrator rights that the Users screen still labels “Subscriber”.

Subscriber, as shipped

wp_user_roles · default

Capabilities
read · level_0
Users screen shows
Subscriber
Can install plugins
No

Subscriber, role edited

wp_user_roles · tampered

Capabilities
read · manage_options · install_plugins · edit_plugins
Users screen shows
Subscriber
Can install plugins
Yes — every subscriber

One user, extra capability

wp_capabilities usermeta

Capabilities
subscriber role + manage_options
Users screen shows
Subscriber
Can change settings
Yes — that account only
Editing the role affects every user who holds it; editing one user’s capabilities affects only that account. Both leave the Users screen showing “Subscriber”. Illustrative reconstruction.

Read the roles

Read-only checks on the role definitions

WP-CLI loads WordPress to run these; --skip-plugins and --skip-themes keep plugin and theme code from running, although must-use plugins still load. If you suspect code in mu-plugins, read the database directly with the SQL below instead.

  1. 01

    List the roles that exist

    Every role the site defines, by display name and key:

    WP-CLI
    wp role list --skip-plugins --skip-themes

    Expect the five WordPress roles plus those your plugins document: WooCommerce adds shop_manager and customer, for example. A role nobody can attribute to an installed plugin is the first finding.

  2. 02

    List each role’s capabilities

    Start with the roles ordinary users hold; the customer role exists only where WooCommerce is installed:

    WP-CLI
    wp cap list subscriber --skip-plugins --skip-themes
    wp cap list customer --skip-plugins --skip-themes

    A default subscriber has read and level_0, nothing else. Capabilities that matter most if they turn up on a low role: manage_options, install_plugins, activate_plugins, edit_plugins, edit_themes, edit_users, create_users, promote_users, delete_users, unfiltered_html and update_core.

  3. 03

    Keep a copy of the raw option

    The whole role definition as JSON, saved outside the web root for the record:

    shell
    mkdir -p ~/incident
    wp option get wp_user_roles --format=json --skip-plugins --skip-themes > ~/incident/user_roles.json

    This is the evidence of what was changed. Take it before any reset.

  4. 04

    Or read it straight from the database

    The same option, without loading WordPress at all:

    SQL · read-only
    SELECT option_value
    FROM wp_options
    WHERE option_name = 'wp_user_roles';

    The value is serialised. Read it as text; do not edit it in place, because changing any string’s length without updating its recorded length breaks the whole option.

Read the users

Find accounts whose own capabilities were changed

A role can be clean while a single account carries extra rights. These checks compare what each user is labelled with against what they actually hold.

  1. 01

    List every account with administrator rights in usermeta

    Compare this list with the Users screen; a difference means the screen is being filtered:

    SQL · read-only
    SELECT u.ID, u.user_login, u.user_email, u.user_registered
    FROM wp_users u
    JOIN wp_usermeta m ON m.user_id = u.ID
    WHERE m.meta_key = 'wp_capabilities'
      AND m.meta_value LIKE '%administrator%';

    Accounts hidden from the Users screen are covered in the rogue-administrator guide.

  2. 02

    Find capabilities entries that hold more than one role

    A user with a single role stores exactly one entry. This lists everyone with more — a second role, or individual capabilities added on top:

    SQL · read-only
    SELECT u.ID, u.user_login, m.meta_value
    FROM wp_users u
    JOIN wp_usermeta m ON m.user_id = u.ID
    WHERE m.meta_key = 'wp_capabilities'
      AND m.meta_value NOT REGEXP '^a:1:[{]s:[0-9]+:"[a-z0-9_-]+";b:1;[}]$';

    Membership and LMS plugins assign extra roles legitimately, so read each result rather than treating the list as a verdict.

  3. 03

    Read one account’s effective capabilities

    Everything a given user can do, from their role and their own entry combined:

    WP-CLI
    wp user list-caps USER --skip-plugins --skip-themes
    wp user meta get USER wp_capabilities --skip-plugins --skip-themes

    On multisite, also list network super administrators with wp super-admin list: they hold every capability on every site, whatever their per-site role says.

Do not miss this

Capabilities can also be granted by code, with a clean database

WordPress asks the user_has_cap and map_meta_cap filters before every permission check, and any plugin, theme or must-use plugin can hook them. A backdoor can grant itself manage_options for one cookie or one user ID at runtime, so the database looks untouched while the account behaves like an administrator.

If an account demonstrably reaches screens its stored capabilities do not allow, search the code rather than the database. A match outside a plugin you installed and can name — in mu-plugins, a drop-in, uploads or the active theme — is the next lead:

shell
grep -rnE "user_has_cap|map_meta_cap" wp-content/

Many legitimate plugins use these filters, so read the matches rather than counting them.

Put it back

Restore the roles without breaking plugin roles

Preserve first — the JSON copy above and a database export. Then correct the roles, the accounts, and the access the escalation was used for.

  1. 01

    Export the database

    A full copy outside the web root, so nothing you change is unrecoverable:

    WP-CLI
    wp db export ~/incident/before-roles-reset.sql

    Keep it with the user_roles.json copy.

  2. 02

    Reset the WordPress default roles

    WP-CLI restores the five default roles to their shipped capabilities; one role at a time, or all of them:

    WP-CLI
    wp role reset subscriber
    wp role reset --all

    This touches only administrator, editor, author, contributor and subscriber. Roles added by plugins — WooCommerce’s shop_manager and customer, membership roles — are left as they are, so check those against the plugin’s documentation or a clean install of the same version.

  3. 03

    Remove roles nobody can account for

    Move anyone holding the role to an appropriate one first, then delete it:

    WP-CLI
    wp user set-role USER subscriber
    wp role delete ROLE_KEY

    set-role replaces a user’s roles but keeps individually granted capabilities, which the next step deals with.

  4. 04

    Strip capabilities added to individual users

    Remove each one the user should not have:

    WP-CLI
    wp user remove-cap USER manage_options

    Re-run the usermeta query afterwards until every remaining multi-entry account has an explanation.

  5. 05

    Treat the escalation as a compromise

    Review what those accounts did: new plugins, edited files, new users, exported data. Rotate administrator and application passwords and the WordPress salts, and find the write path that changed the roles before declaring it closed.

Common questions

Common questions about tampered roles

Can a plugin legitimately change wp_user_roles?

Yes. Plugins add roles and capabilities all the time — WooCommerce, membership, LMS and role-editor plugins among them. The question is whether every change has an owner. A capability on the subscriber role that no installed plugin documents does not.

Will wp role reset --all delete my WooCommerce roles?

No. It resets only the five default WordPress roles to their shipped capabilities and does not touch roles that plugins added. Check those separately against the plugin’s own documentation.

The Users screen shows no extra administrators. Am I fine?

Not necessarily. Elevated rights can sit in a role definition, in one user’s capabilities, or in a code filter, and none of those changes the role name the screen shows. Run the role and usermeta checks above.

How did someone change the roles without an admin account?

Through any write to the database: an exploited plugin, SQL injection, a stolen login with enough rights, or code already running on the server. The change itself tells you there was a write path; finding it is part of the response.

Is it safe to edit the option in phpMyAdmin?

Not by hand. The value is serialised, and changing a string without correcting its recorded length makes WordPress discard the whole option — taking every role with it. Use WP-CLI’s role and capability commands instead.