Critical Vulnerability Patched in UserPro Plugin

Published 22 May 2024
Table of Contents

This blog post is about the UserPro plugin vulnerabilities. If you’re a UserPro user, please update the plugin to at least version 5.1.9.

All paid Patchstack users are protected from this vulnerability. Sign up for the free Community account first, to scan for vulnerabilities and apply protection for only $5 / site per month with Patchstack. For plugin developers, we have security audit services and Enterprise API for hosting companies.

About the UserPro Plugin

The plugin UserPro (premium version), which has over 20,000 sales, is known as the more popular community and user profile plugin in WordPress. This plugin is developed by DeluxeThemes.

This premium WordPress plugin is a full-featured user profile and community plugin. Users are able to create front-end user profiles and community sites in WordPress using UserPro. It comes packed with features like customizable login and registration forms and social connect integration.

The security vulnerability

This plugin suffers from an unauthenticated account takeover vulnerability. This allows any unauthenticated users to change the password of any users with certain conditions. The described vulnerability was fixed in version 5.1.9 and assigned CVE-2024-35700.

The underlying vulnerability exists on userpro_process_form function:

function userpro_process_form()
{

    global $userpro;

    /* Security do not use noonce to non-looged in users */
    $template = $_POST['template'];

    if ( $template !== 'login' && $template !== 'register' ) {
        if ( ! check_ajax_referer( 'user_pro_nonce', 'nonce', false ) ) {
            wp_send_json_error( 'Invalid nonce.' );
            die();
        }
    }


    if (!isset($_POST) || $_POST['action'] != 'userpro_process_form') {
        die();
    }

    if (!userpro_is_logged_in() && $_POST['template'] == 'edit') {
        die();
    }

    /* Form */
    $form = [];
    foreach ($_POST as $key => $val) {
        $key = explode('-', $key);
        $key = $key[0];
        $form[$key] = $val;
    }

    $shortcode = $_POST['shortcode'];
    $user_id = isset($form['user_id']) ? $form['user_id'] : '';

    /* Runs before a form is processed */
    do_action('userpro_before_form_save', $form);
    $output = [];

    /* PROCESSING ACTIONS */
    switch ($template) {
------- CUT HERE -------
        /* change pass */
        case 'change':
            $output['error'] = [];

            if (!$form['secretkey']) {
                $output['error']['secretkey'] = __('You did not provide a secret key.', 'userpro');
            }

            /* Form validation */
            /* Here you can process custom "errors" before proceeding */
            $output['error'] = apply_filters('userpro_form_validation', $output['error'], $form);

            if (empty($output['error'])) {

                $users = get_users([
                    'meta_key' => 'userpro_secret_key',
                    'meta_compare' => 'EXISTS', // Check if the key exists in metadata.
                ]);

                $key_matched = false;
                $user_id = null;

                foreach ($users as $user) {
                    $stored_hashed_key = get_user_meta($user->ID, 'userpro_secret_key', true);

                    // Verify the input key directly without additional manipulation
                    if (wp_check_password($form['secretkey'], $stored_hashed_key)) {
                        $key_matched = true;
                        $user_id = $user->ID;
                        break; // Exit the loop as we found a matching key
                    }
                }

                if (!$users[0]) {
                    $output['error']['secretkey'] = __('The secret key is invalid or expired.', 'userpro');
                } else {
                    add_filter('send_password_change_email', '__return_false');
                    $user_id = $users[0]->ID;
                    wp_update_user(['ID' => $user_id, 'user_pass' => $form['user_pass']]);
                    delete_user_meta($user_id, 'userpro_secret_key');

                    add_action('userpro_pre_form_message', 'userpro_msg_login_after_passchange');
                    $shortcode = stripslashes($shortcode);
                    $modded = str_replace('template="change"', 'template="login"', $shortcode);
                    $output['template'] = do_shortcode($modded);
                    if (userpro_get_option('notify_user_password_update') == "1") {
                        userpro_mail($user_id, 'passwordchange', $form['user_pass']);
                    }
                }
            }

            break;
------- CUT HERE -------

This function itself is hooked to the wp_ajax_nopriv_userpro_process_form function making it accessible by unauthenticated users. The above case handles the process of changing the user’s password. Let’s try to analyze the process.

First, the function will construct a $users object using the get_users function specifying the “meta_key” to “userpro_secret_key”. This indicates that the object will contain any users on the database that have the “userpro_secret_key” metavalue set to the account. The value itself is just a secret key that will be generated when users try to change their password and will be used to verify the password change process.

The function then performs a loop to check if the supplied $form[‘secretkey’] value is the same as the $stored_hashed_key value (which is the user’s “userpro_secret_key”) using the wp_check_password and it will set the $key_matched variable to true and set the $user_id variable to the matched user’s id.

The above check seems legit, however, the next step in the function is checking if $users[0] exists and it will set the $user_id variable to the $users[0]->ID. It will then perform a wp_update_user function, changing the $users[0] (the first entry of users that have the “userpro_secret_key” set) password with the supplied $form[‘user_pass’] value.

With this condition, any unauthenticated users are able to change the password of any user that has a “userpro_secret_key” value set. The scenario of exploitation would be ideally performed after a user requests a password change via the forgot password feature and before the user actually changes their password.

Note that this vulnerability is reproducible in a default installation and activation of the UserPro plugin without a specific requirement or configuration.

The patch

The vendor applies a patch by first checking if the new password is the same as the current one. Then, the code will directly use the $user_id variable that has been set from the previous check using the wp_check_password function.

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for changing the user’s password. Always make sure the object or variable passed to the crucial function to update the user’s password has been validated and previously checked.

Timeline

17 March, 2024We found the vulnerability and reached out to the plugin vendor.
18 March, 2024We reach out to the plugin vendor.
29 April, 2024UserPro version 5.1.9 released to patch the reported issue.
21 May, 2024Added the vulnerabilities to the Patchstack vulnerability database.
22 May, 2024Security advisory article publicly released.

The latest in Security advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu