# wordpress.org Rosetta: invite stores unvalidated role — Editor can assign site Administrator despite editable_roles

Rosetta mu-plugin stored invite role from unvalidated $_REQUEST['role'], letting an Editor invite an account as site Administrator despite editable_roles removing it.

- Payout: hidden
- Severity: high
- Program: WordPress
- Found by: 0xbs0d27
- Disclosed: 2026-11-05
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/3942522
- Library page: https://bugbountylibrary.com/r/h1%3A3942522
- Tags: area:web domain:web impact:privesc kind:bounty platform:hackerone sev:high tech:php tech:wordpress technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** WordPress Multisite mu-plugin invite handler (admin_action_adduser) on *.wordpress.org locale sites
**Root cause:** wporg-add-existing-users-via-username.php persists the invite option with role taken directly from $_REQUEST['role'] before consulting get_editable_roles(), and Core's add_existing_user_to_blog() applies the stored role via set_role() without wp_ensure_editable_role().
**Steps:**
1. As an Editor on a Rosetta locale site (promote_users granted, administrator removed from editable_roles), POST to admin_action_adduser with `role=administrator` for an existing network user.
2. Mu-plugin runs add_option('new_user_'+substr(md5(user_id),0,5), array('user_id'=>..., 'role'=>$_REQUEST['role'])) storing administrator unvalidated.
3. Victim accepts invite at /newbloguser/{key}/, triggering maybe_add_existing_user_to_blog() -> add_existing_user_to_blog() -> add_user_to_blog() -> set_role('administrator').
4. Confirm wp_capabilities contains administrator and manage_options is true.
**How found:** Source review of Rosetta's editable_roles filter alongside the mu-plugin invite handler; noticed role was read from $_REQUEST before get_editable_roles() and that Core's apply path lacks wp_ensure_editable_role(), then validated in a local Multisite lab.
**Exploitation:** Authenticated Editor submits role=administrator in the invite request (not offered in UI dropdown), generating a valid invite key; when the invited account accepts, Core applies the stored role, granting site Administrator (manage_options) on that blog.
**Escalation:** Editor escalates a controlled or other network account to site Administrator, gaining full control of that locale site (not network super-admin).
**Insight:** When a plugin adds a delegated user-management feature, check whether role/privilege values are validated against get_editable_roles() at persistence time, not just at render time; the apply path (set_role) often skips re-checks.

## Proof of concept (copied exactly from the original report)

**Mu-plugin persists unvalidated role from request**

```
$new_user_key = substr( md5( $user_id ), 0, 5 );
add_option( 'new_user_' . $new_user_key, array(
    'user_id' => $user_id,
    'email'   => $user_details->user_email,
    'role'    => $_REQUEST['role'],   // UNVALIDATED
) );
$roles = get_editable_roles();        // too late — only used for email text
$role  = $roles[ $_REQUEST['role'] ];
// wp_mail( … translate_user_role( $role['name'] ) … )
```

**Core applies stored role without re-check**

```
function add_existing_user_to_blog( $details = false ) {
    if ( is_array( $details ) ) {
        $result = add_user_to_blog( get_current_blog_id(), $details['user_id'], $details['role'] );
        // …
    }
}
// add_user_to_blog() → $user->set_role( $role );  // no wp_ensure_editable_role()
```

**Rosetta editable_roles removes administrator**

```
public function remove_administrator_from_editable_roles( $roles ) {
    if ( ! is_super_admin() ) {
        unset( $roles['administrator'] );
    }
    return $roles;
}
```

