CVE-2023-41954: ProfilePress <= 4.13.1 — Unauthenticated Privilege Escalation

Privilege Escalation vulnerability on ProfilePress Plugin. Malicious actor could register an account as Editor, Author, or other existing roles. The CVSS of this vulnerability is 8.6 (High), so it is dangerous for many 200k+ sites that are using this plugin.
About Plugin

- Plugin name: Paid Membership Plugin, Ecommerce, Registration Form, Login Form, User Profile & Restrict Content — ProfilePress
- Affected version: <= 4.13.1
- Plugin home page: https://wordpress.org/plugins/wp-user-avatar/
ProfilePress is a WordPress plugin that allows you to customize the user registration and profile forms, restrict content, and sell memberships and digital products. It is a premium plugin, but it offers a free version with limited features.
Vulnerable Code
- Filename:
/wp-content/plugins/wp-user-avatar/src/Classes/AjaxHandler.php
- Code:
// This function is started on line 441
function ajax_signup_func()
{
if ( ! defined('W3GUY_LOCAL') && is_user_logged_in()) wp_send_json_error();
if (isset($_REQUEST)) {
$is_melange = ( ! empty($_POST['is_melange']) && $_POST['is_melange'] == 'true');
$form_id = ! empty($_POST['melange_id']) ? $_POST['melange_id'] : @$_POST['signup_form_id'];
$form_id = absint($form_id);
$redirect = ppressPOST_var('signup_redirect', '', true);
if ( ! empty($_POST['melange_redirect'])) {
$redirect = sanitize_text_field($_POST['melange_redirect']);
}
$no_login_redirect = sanitize_text_field(@$_POST['signup_no_login_redirect']);
// if this is tab widget.
if (isset($_POST['is-pp-tab-widget']) && $_POST['is-pp-tab-widget'] == 'true') {
$widget_status = @TabbedWidgetDependency::registration(
$_POST['tabbed-reg-username'],
$_POST['tabbed-reg-password'],
$_POST['tabbed-reg-email']
);
if ( ! empty($widget_status)) {
$response = '<div class="pp-tab-status">' . $widget_status . '</div>';
}
} else {
$response = RegistrationAuth::register_new_user($_POST, $form_id, $redirect, $is_melange, $no_login_redirect);
}
// display form generated messages
if ( ! empty($response)) {
if (is_array($response)) {
$ajax_response = ['redirect' => $response[0]];
} else {
$ajax_response = ['message' => html_entity_decode($response)];
}
wp_send_json($ajax_response);
}
}
wp_die();
// Closed on line 488
}
Function ajax_signup_func() will handle the registration process that are customized form by admin. And then this function call the register_new_user() function from the RegistrationAuth class for inserting new user to database.
So.. Where’s the problem? The problem is on the RegistrationAuth class. Let me show you the code… ^^
- Filename:
/wp-content/plugins/wp-user-avatar/src/Classes/RegistrationAuth.php
- Code:
// This function is started on line 98
public static function register_new_user($post, $form_id = 0, $redirect = '', $is_melange = false, $no_login_redirect = '')
{
if ( ! get_option('users_can_register')) return;
$files = $_FILES;
// create an array of acceptable userdata for use by wp_insert_user
$valid_userdata = array(
'reg_username',
'reg_password',
'reg_password2',
'reg_email2',
'reg_password_present',
'reg_email',
'reg_website',
'reg_nickname',
'reg_display_name',
'reg_first_name',
'reg_last_name',
'reg_bio',
'reg_select_role',
);
// get the data for userdata
$segregated_userdata = array();
// loop over the $_POST data and create an array of the wp_insert_user userdata
foreach ($post as $key => $value) {
if ($key == 'reg_submit') {
continue;
}
if (in_array($key, $valid_userdata)) {
if (in_array($key, ['reg_email', 'reg_email2'])) {
$segregated_userdata[$key] = sanitize_email($value);
continue;
}
// sanitize_textarea_field is used to preserve any line breaks
$segregated_userdata[$key] = sanitize_textarea_field($value);
}
}
$email = isset($segregated_userdata['reg_email']) ? $segregated_userdata['reg_email'] : '';
$email2 = isset($segregated_userdata['reg_email2']) ? $segregated_userdata['reg_email2'] : null;
// get convert the form post data to userdata for use by wp_insert_users
$username = isset($segregated_userdata['reg_username']) ? $segregated_userdata['reg_username'] : '';
// Handle username creation when username requirement is disabled.
if (ppress_is_signup_form_username_disabled($form_id, $is_melange)) {
$username = sanitize_user(current(explode('@', $email)), true);
// Ensure username is unique.
$append = 1;
$o_username = $username;
while (username_exists($username)) {
$username = $o_username . $append;
$append++;
}
}
$username = apply_filters('ppress_registration_username_value', $username);
$password = apply_filters('ppress_registration_password_value', isset($segregated_userdata['reg_password']) ? $segregated_userdata['reg_password'] : '');
$flag_to_send_password_reset = false;
// if the reg_password field isn't present in registration, generate a password for the user and set a flag to send a password reset message
if (empty($password) && (empty($segregated_userdata['reg_password_present']) || $segregated_userdata['reg_password_present'] != 'true')) {
$password = wp_generate_password(24);
$flag_to_send_password_reset = apply_filters('ppress_enable_auto_send_password_reset_flag', true);
}
$password2 = isset($segregated_userdata['reg_password2']) ? $segregated_userdata['reg_password2'] : null;
$website = isset($segregated_userdata['reg_website']) ? $segregated_userdata['reg_website'] : '';
$nickname = isset($segregated_userdata['reg_nickname']) ? $segregated_userdata['reg_nickname'] : '';
$display_name = isset($segregated_userdata['reg_display_name']) ? $segregated_userdata['reg_display_name'] : '';
$first_name = isset($segregated_userdata['reg_first_name']) ? $segregated_userdata['reg_first_name'] : '';
$last_name = isset($segregated_userdata['reg_last_name']) ? $segregated_userdata['reg_last_name'] : '';
$bio = isset($segregated_userdata['reg_bio']) ? $segregated_userdata['reg_bio'] : '';
$role = isset($segregated_userdata['reg_select_role']) ? $segregated_userdata['reg_select_role'] : '';
// Closed on line 429
}
Focus on the variable $valid_userdata, it’s array of valid field from variable $post. And we know there is any “reg_select_role” on the array value. And then, if there is any reg_select_role field on the registration process, it will be assign on the $role variable. Then Malicious actor can manipulate the registration process with adding reg_select_role field.
PATCH
This vulnerability has been patched on version 4.13.2 with checking the form structures that is defined from admin dashboard.

Attack Scenario
1. Install WordPress and run on your local server. Then install Plugin ProfilePress <= 4.13.1
2. Setup registration form and select default Role.

3. Publish form on a Page.

4. Open the registraion page on Incognito tab.
5. Fill the forms and submit.

6. Manipulate the request body with adding “reg_select_role” field with value editor.

7. Back to Admin Dashboard and check the role of registered User.

Timeline
- 07 September, 2023: Reported to Patchstack
- 09 September, 2023: Vulnerability Fixed
- 12 September, 2023: Publicly Disclosed
Support
