DEV Community

Hamza Ahmad Aslam
Hamza Ahmad Aslam

Posted on Originally published at hamzaahmadaslam.com

Audit WordPress plugin capabilities after installing plugins

Originally published at hamzaahmadaslam.com.

How this article was made: drafted with AI assistance from a researched brief, then checked against primary documentation before publishing.

Audit WordPress plugin capabilities by recording each role before you install or update a plugin, recording it again afterward, and testing the tasks that changed. Compare Administrator, Editor, and every custom role you rely on, then sign in with representative test accounts and request protected pages or endpoints directly. A hidden menu item does not prove the underlying request is blocked.

Start the WordPress plugin capabilities audit with a baseline

A WordPress role is a set of capabilities, and a capability represents something that user may be allowed to do. WordPress builds the stored role option name from the site's database prefix plus user_roles, as shown by the WP_Roles class. With the default wp_ prefix, that is wp_user_roles; on multisite, a site can have a per-site key such as wp_2_user_roles.

On multisite, add --url=<site-url> to every wp role, wp cap, and wp user list-caps command so WP-CLI targets the site whose roles you are auditing. The WP-CLI global parameter documentation specifies --url as the way to select the target site on multisite, so do not omit it when auditing a subsite.

Treat this as a WordPress user roles audit with a narrow purpose. You want to know what a plugin installation or update changed, which users inherited those changes, and whether the resulting access matches the work they should perform.

Run the plugin installation or update and any state-changing authorization tests on a staging copy. You can capture a production capability inventory when you need it for comparison, but do not use live save, delete, or export actions as the test bed.

Do this before changing the plugin. First list every role so custom roles are included rather than guessed from their display names.

wp role list --fields=role --format=csv
Enter fullscreen mode Exit fullscreen mode

Choose the roles that matter to the plugin. Administrator and Editor are useful reference points, but include every custom role whose users may interact with the feature.

Capture each role with wp cap list. The --show-grant flag includes capabilities defined for the role together with whether they are granted.

Store these files outside the public document root. The illustrative commands below use $HOME/cap-audit and an illustrative WordPress path. Create that directory first, replace /path/to/wordpress with the site's WordPress path, and use the same approach if your WP-CLI configuration already supplies the path.

wp --path=/path/to/wordpress cap list administrator --show-grant --format=csv > "$HOME/cap-audit/caps-before-administrator.csv"
wp --path=/path/to/wordpress cap list editor --show-grant --format=csv > "$HOME/cap-audit/caps-before-editor.csv"
wp --path=/path/to/wordpress cap list custom_role --show-grant --format=csv > "$HOME/cap-audit/caps-before-custom-role.csv"
Enter fullscreen mode Exit fullscreen mode

custom_role is an illustrative placeholder. Replace it with the role key returned by wp role list.

After installing or updating the plugin on staging, take a second snapshot.

wp --path=/path/to/wordpress cap list administrator --show-grant --format=csv > "$HOME/cap-audit/caps-after-administrator.csv"
wp --path=/path/to/wordpress cap list editor --show-grant --format=csv > "$HOME/cap-audit/caps-after-editor.csv"
wp --path=/path/to/wordpress cap list custom_role --show-grant --format=csv > "$HOME/cap-audit/caps-after-custom-role.csv"
Enter fullscreen mode Exit fullscreen mode

Keep both sets outside the document root. If you captured relative-path files while your shell was inside a public web root, move them out of that directory before continuing.

Identify added permissions before deciding who should get them

Compare each before file with its matching after file. Record capabilities that appeared, disappeared, or changed grant state.

Do not assume every new capability should be copied to every custom role. WordPress custom capabilities can be added to roles, but the capability matters only when code uses it as a permission check.

This illustrative worksheet shows the shape of the review. The capability assignments and plugin-specific capability name are examples, not measured results.

Illustrative capability Administrator before Administrator after Editor before Editor after Custom role before Custom role after Review
manage_options granted granted absent absent absent absent Illustrative unchanged core permission
edit_posts granted granted granted granted granted granted Illustrative unchanged content permission
upload_files granted granted granted granted absent absent Illustrative unchanged media permission
demo_plugin_manage_reports absent granted absent absent absent absent Illustrative plugin-specific addition. Decide which roles need the related task

The last row is deliberately illustrative. Your plugin may use another capability, reuse a core capability, or introduce several permissions.

For every changed row, write down the task it gates. Examples include viewing a settings screen, saving configuration, managing a plugin-owned content type, exporting data, or calling a private API endpoint.

If you cannot connect a capability to a task, inspect the plugin documentation or code before granting it. A capability name alone does not tell you the full effect of adding it to a role.

Validate custom roles instead of trusting their names

A role called store_staff, content_manager, or support_agent tells you its label, not its current permissions. Check the capability set itself.

You can also inspect a representative account with wp user list-caps. The command accepts a user ID, email address, or login and lists the stored capabilities assembled for that user from role capabilities and user-level capability data.

wp user list-caps test-administrator
wp user list-caps test-editor
wp user list-caps test-custom-role
Enter fullscreen mode Exit fullscreen mode

The three account names are illustrative staging accounts. Use dedicated test accounts on staging that represent the roles you are checking.

If a test account must exist on production, give it a strong unique password and only the permissions needed for its test. Do not create a spare Administrator account on production for this audit. Use an existing administrator for allowed administrator checks, then delete or demote temporary test accounts when the audit is complete.

This answers a common question: yes, you can get a list of stored capabilities for each user type. Use wp cap list when you want the role definition. Use wp user list-caps when you want the role capabilities plus capabilities stored directly for that account.

That distinction matters because WordPress can also store capabilities directly on an individual user. Two accounts associated with the same role therefore need not have identical stored permissions if one has user-specific capability changes.

The snapshots still do not prove effective access. During a capability check, WordPress can map a requested capability with map_meta_cap() and plugins can dynamically alter the result through the user_has_cap filter. Those runtime changes do not have to appear in the stored role or user capability lists.

For authorization checks in code, WordPress provides current_user_can(). Its documentation also discourages checking a role name in place of a capability. Test the permission that represents the action instead of writing rules around labels such as "Editor."

Test representative accounts through the UI and direct requests

For WordPress plugin permissions, a visible or hidden control is only one part of the test. The request that loads or changes data must enforce the intended capability too.

WordPress add_menu_page() documentation states that its capability argument controls whether the page is included in the menu. It also says the function handling that page's output must check the required capability.

Use that distinction in your test procedure.

For each representative account:

  1. Sign in with only the role and user-specific permissions you intend to test.
  2. Check whether the relevant menu, button, or editor control appears.
  3. For a plain protected screen such as admin.php?page=demo-plugin, copy its URL from an account that is allowed to use it.
  4. Sign back in as the account that should be denied and request that plain screen URL directly.
  5. For actions that save, delete, export, or otherwise change data, submit the normal action against disposable staging data.
  6. Record the expected result before running the test, then record whether the behavior matches it.

Treat URLs and forms containing _wpnonce differently. A WordPress nonce is tied to an action, user, user session, and time window. Replaying a nonce created for the allowed account from the denied account can fail nonce verification before the capability check runs.

That failure does not prove authorization is correct. To test the capability check, make the request with a nonce valid for the denied account, such as one rendered in that account's own session when the relevant action is available. If you cannot generate the action for that account, inspect the handler and confirm that current_user_can() runs. Plain admin.php?page=... screen URLs without a nonce do not have this problem.

REST routes need another step. Authenticate the REST request as the exact account whose permissions you are testing. A browser request that sends WordPress cookies without a REST nonce is treated as unauthenticated, so it cannot prove what an Editor or custom-role account is allowed to do.

WordPress cookie authentication for the REST API requires a nonce created with the wp_rest action. Send it as the X-WP-Nonce header or the _wpnonce parameter. Without that nonce, WordPress sets the current REST user to ID 0, even when the browser has login cookies.

Another option is Application Passwords, which have been in WordPress core since version 5.6. The REST API authentication documentation documents sending an Application Password with HTTP Basic Authentication over HTTPS.

This command is illustrative. Replace the username, Application Password placeholder, domain, and route.

curl --user "test-reporter:APP_PASSWORD" https://example.com/wp-json/demo-plugin/v1/reports
Enter fullscreen mode Exit fullscreen mode

For an endpoint that follows the core pattern of returning rest_forbidden with rest_authorization_required_code(), a logged-in user who lacks permission receives HTTP 403. An unauthenticated request receives HTTP 401. For a denied-role test, confirm that the failure belongs to the authenticated test account rather than an anonymous request.

Here is a copyable test matrix. Every scenario, account, URL, capability, and result below is illustrative.

Illustrative scenario Illustrative account Capability under review UI expectation Direct request Expected authorization
View demo plugin settings test-editor manage_options Menu hidden /wp-admin/admin.php?page=demo-plugin Denied
View demo plugin settings test-admin manage_options Menu visible /wp-admin/admin.php?page=demo-plugin Allowed
View demo reports test-reporter demo_plugin_manage_reports Reports control visible /wp-admin/admin.php?page=demo-plugin-reports Allowed
Request demo REST report test-editor demo_plugin_manage_reports No report control Authenticated request to /wp-json/demo-plugin/v1/reports Denied by the permission check; expected HTTP 403 for the illustrative rest_forbidden handler
Request demo REST report test-reporter demo_plugin_manage_reports Report control visible Authenticated request to /wp-json/demo-plugin/v1/reports Allowed

Do not treat a hidden control, failed nonce, or anonymous REST request as a passing authorization test. If a user who should be denied can load the direct page, submit the action, or receive protected endpoint data after authenticating correctly, the permission check still needs correction.

Decide whether new capabilities belong on custom roles

When a new plugin adds custom capabilities, existing custom roles do not gain them merely because their names suggest similar duties. Check the new capability against the work each role should perform.

You do not have to manually enable every new capability on a newly created role. Grant one when the role needs the task protected by that capability. Leave it absent when the task is outside that role's job.

For example, suppose the illustrative demo_plugin_manage_reports capability appears after installation. If only a reporting role should use that feature, grant it to that role rather than expanding Editor as a shortcut.

WP-CLI provides wp cap add for adding capabilities to a role:

# Illustrative role and capability. Replace both before running.
wp cap add reporting_staff demo_plugin_manage_reports
Enter fullscreen mode Exit fullscreen mode

It also provides wp cap remove when a role received a capability it should not retain:

# Illustrative role and capability. Replace both before running.
wp cap remove editor demo_plugin_manage_reports
Enter fullscreen mode Exit fullscreen mode

If a plugin protects its screen with an existing broad capability instead of a plugin-specific one, identify that requirement before changing the role. Granting a broad permission solely to expose one plugin screen can also expose other tasks that use the same capability.

That is why the decision starts with the task and its authorization check, not with the menu item you want to make visible.

Correct the smallest affected permission and retest the task

After changing a role, take another capability snapshot. Confirm that only the intended rows changed.

Then repeat the same representative-account tests. Do not stop after the control appears or disappears. Retest the direct page, the state-changing action, and any private endpoint involved in that task.

Use this checklist for each correction:

  • Record the role and capability you changed.
  • State the task that requires the capability.
  • Confirm the updated role definition.
  • Confirm the representative user's stored role and user-level capabilities.
  • Test the normal UI path.
  • Test the direct request while authenticated as an account that should be denied.
  • For nonce-protected actions, make sure the denial is not merely a nonce failure.
  • Test the allowed account against the same task.
  • Record the expected and resulting authorization outcome.

The direct-request test is the step that confirms effective access when a plugin changes capability decisions dynamically through map_meta_cap or user_has_cap.

This gives you a repeatable audit trail without turning the exercise into a full security review.

Put the permission audit into routine maintenance

Keep the before files, after files, capability matrix, and test matrix with the plugin change record. Re-run the procedure when an installation or update introduces new admin screens, data actions, content types, or private endpoints.

Remove or demote temporary production test accounts after the audit. WordPress's least-privilege guidance is to give users only the privileges needed for their work.

Keep broader account and site controls in the WordPress security hardening checklist. If permission drift and post-change role testing need an ongoing owner, WordPress maintenance and security can include that operational review.

Top comments (0)