Proof/CVE-2026-16534
WordPress blocks a lesser role from minting an administrator through the admin screens. The Import and Export Users and Customers plugin took the same actor's CSV file and wrote the rows straight into wp_insert_user — so a role that could only create users could hand itself an administrator, or rewrite an existing admin's password.
WordPress does not treat "create a user" and "promote a user to administrator" as the same permission. Core maps them to distinct capabilities — create_users, promote_users, edit_users — and adds a hard rule that a non-administrator can never edit or elevate an administrator. A bulk-import feature is only safe if it re-checks those same capabilities for the person clicking Import.
A user who holds only the user-creation capability must not be able to assign the administrator role, nor edit an account that already holds it.
The bug was not that the plugin could import users. The bug was that the import path trusted the CSV's role, user_pass and target-identity columns as data, while WordPress core treats acting on those same fields as privileged operations gated by the current user's capabilities.
The import handler confirmed the actor could reach the import screen and create users, then looped over rows and called WordPress's user APIs with the CSV's own field values. What it never did was re-map the role change or the edit of an existing account against the acting user — the check core performs on every admin-screen equivalent.
import screen:
require current_user_can('create_users') // gate present
per row:
role = row['role'] // taken verbatim
target = existing user? update : insert
wp_insert_user([... 'role' => role]) // no promote_users check
wp_update_user([... 'user_pass' => row['user_pass'],
'user_email' => row['user_email']])
// no edit_user(target) check
// missing: current_user_can('promote_users')
// missing: current_user_can('edit_user', target_admin_id)Core's own map_meta_cap() would have refused both operations for a lesser role: promoting to administrator requires promote_users, and editing an existing administrator is denied outright to anyone who is not one. By calling the low-level functions directly, the importer skipped the layer that enforces those rules.
A custom role with import access and create_users, nothing more.
Role column set to administrator; or an admin's login plus a new password.
Rows passed to wp_insert_user / wp_update_user unchecked.
New administrator, or an existing admin's credentials overwritten.
A custom or vendor role (shop manager, staff onboarding, LMS instructor) that can reach the plugin's import screen and create accounts, but cannot promote users or edit administrators through wp-admin.
The same actor tries to set a new user's role to administrator in the normal Users screen and is refused. This proves core's capability boundary is present and enforced.
A row sets role=administrator for a new account, or names an existing administrator by login/ID and supplies a new user_pass / user_email.
The handler passes each row to wp_insert_user / wp_update_user. No promote_users check on the role, no per-target edit check on the administrator.
The attacker logs in to the freshly minted administrator, or authenticates as the existing admin whose password they just overwrote. Full site control follows.
The finding was promoted only after the vulnerable path, the negative control and the fixed control were retained together against the same actor and site.
The user-creation role is refused when it tries to set administrator or edit an admin in wp-admin.
The same role imports a CSV; an administrator is created and an existing admin's credentials are overwritten.
The privileged row is rejected because the import now enforces role-assignment and per-user edit capabilities.
A capability check that guards the screen is not the same as a capability check that guards the operation. The import screen asked "can this user create accounts?" and then performed operations — promotion, and editing an administrator — that demand far stronger permissions.
The durable engineering rule is that bulk and API paths must re-map every privileged field against the acting user with the same current_user_can() checks the interactive path uses. A CSV column is attacker-controlled input, not an authorization decision.
The workflow did not stop at "the import writes users." Each stage had to produce evidence, or a refutation, before the candidate could move forward.
Trace the CSV role and target-identity columns into the user-write calls and isolate every capability check on the path.
Challenge the assumption that a create-users gate is sufficient to authorize a promote-to-administrator write.
Stand up a non-admin role and the smallest CSV that both mints an administrator and rewrites an existing one.
Admin-screen negative control, exact privilege marker, and re-run against 2.4.2 as the fixed control.
Review source, replay, impact, version range, wording and public-safety boundary before reporting to the vendor.
WPScan scores CVE-2026-16534 at 7.2 High on CVSS 3.1. The PR:H term reflects that the attacker must already hold a privileged role — one with import access and user creation — not that the impact is limited. Once that foothold exists, confidentiality, integrity and availability are all fully compromised at the administrator level.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
The realistic actor is an inside role or a lower-tier account on a multi-author, membership, e-commerce or LMS site — exactly the deployments that hand out custom roles with user-management slices. Any such role that can reach this import screen becomes an administrator on demand.
| Plugin | Affected range | Fixed |
|---|---|---|
import-users-from-csv-with-meta | ≤ 2.4.1 (all < 2.4.2) | 2.4.2 |
Marketed as Import and Export Users and Customers. Sites that never delegate user management to a non-administrator role are not directly exploitable, but should still upgrade.
The fix is not "block the administrator string in CSV." The durable rule is that the import must authorize each privileged field against the acting user exactly as the interactive path does.
current_user_can('promote_users') before honoring any role in a row, and reject or downgrade rows that request roles the actor cannot grant.current_user_can('edit_user', $id)) before updating an existing account, so administrators cannot be edited by lesser roles.create_users or list-user capabilities.The bug lived between two ideas of "who can do this." WordPress core said a lesser role may not promote users or touch an administrator. The plugin said, in effect, "if you reached the import screen, your rows are trusted." A CSV column carried the privileged instruction across that gap.
The CVE is the identifier. The admin-screen negative control is what proved the role boundary was real before the import erased it.
map_meta_cap() — promote_users / edit_users mappingwp_insert_user() referenceCredit: Discovered and reported by Charles Vosburgh. Research was AI-assisted through source mapping, hypothesis generation, runtime construction and skeptical validation; final review and disclosure ownership remained human.