Thrive Theme Vulnerability: Dismiss Tooltip to Privilege Escalation

Published 29 November 2023
Updated 6 December 2023
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

This blog post is about a premium Thrive Theme vulnerability. If you're a premium Thrive Theme user, please update the plugin to at least version 3.24.0.

You can sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.

For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.

About the Thrive Theme

The theme Thrive (premium version), which is estimated to have over 110,000 active installations, is one of the more popular premium themes for online business purposes.

Thrive Theme

This theme is a premium builder focused on creating an online business. Thrive Theme is a fully-fledged WordPress theme and site builder, with drag and drop, front-end customization power. Thrive Theme allows us to visually create and customize blog post templates, sidebar, page layout, category pages, and more.

The security vulnerability

The Thrive theme suffers from an authenticated privilege escalation vulnerability. This issue occurs because there is a lack of access control and input validation on the Dismiss Tooltip action.

The function utilizes the update_user_meta function, which could lead to a complete privilege escalation if a user can fully control the meta key and value saved. The described vulnerability was fixed in version 3.24.0 and assigned CVE-2023-47782.

Authenticated Privilege Escalation

The underlying vulnerability is located in the action_dismiss_tooltip function:

/**
 * Dismiss user tooltip
 *
 * @return array
 */
public function action_dismiss_tooltip() {
    $response = [];

    $user = wp_get_current_user();
    /* double check, just to be sure */
    if ( $user ) {
        $key   = $this->param( 'meta_key' );
        $value = $this->param( 'meta_value' );
        update_user_meta( $user->ID, $key, $value );
        $response[ $key ] = $value;
    }

    return $response;
}

The function intends to dismiss a popup tooltip for the current user. Unfortunately, it allows users to arbitrarily set the meta key and value passed to the update_user_meta function. This condition could lead to a privilege escalation since the user can modify their wp_capabilities meta which contains the user's role definition.

The action_dismiss_tooltip function itself can be called from the handle function below which is hooked to the wp_ajax_tcb_editor_ajax action:

/**
 * Handles the ajax call
 */
public function handle() {
    if ( wp_verify_nonce( $this->param( 'nonce' ), self::NONCE_KEY ) === false ) {
        $this->error( __( 'This page has expired. Please reload and try again', 'thrive-cb' ), 403, 'nonce_expired' );
    }

    $custom = $this->param( 'custom' );
    if ( empty( $custom ) || ! method_exists( $this, 'action_' . $custom ) ) {
        $this->error( 'Invalid request.', 404 );
    }
    $action = 'action_' . $custom;
    /* restore WAF-protected fields */
    TCB_Utils::restore_post_waf_content();

    /**
     * Action triggered before any handler code is executed
     * Allows setting up everything needed for the request, e.g. global objects
     *
     * @param TCB_Editor_Ajax $instance
     */
    do_action( 'tcb_ajax_before', $this );

    /**
     * Action called just before the custom ajax callbacks.
     *
     * @param {TCB_Editor_Ajax} $this
     */
    do_action( 'tcb_ajax_before_' . $custom, $this );

    $response = call_user_func( array( $this, $action ) );

    $response = apply_filters( 'tcb_ajax_response_' . $custom, $response, $this );

    if ( $this->param( 'expect' ) === 'html' ) {
        wp_die( $response ); // phpcs:ignore
    }

    $this->json( $response );
}

Notice that there is no permission or role check on the function and only nonce validation is implemented. We tried to search if a lower permission role user can fetch the nonce value.

We observed that one of the functions that can generate the nonce value is tcb_auth_check_data function:

/**
 * Add Architect ajax nonce to the after auth data so we can refresh it
 *
 * @param $data
 *
 * @return mixed
 */
function tcb_auth_check_data( $data ) {
	$data ['tcb_nonce'] = wp_create_nonce( TCB_Editor_Ajax::NONCE_KEY );

	return $data;
}

The tcb_auth_check_data function itself is a hooked function to the tvd_auth_check_data filter. We then check that the filter can be called from the login_footer function:

public function login_footer() {
    global $interim_login;

    if ( empty( $interim_login ) || $interim_login !== 'success' || empty( $_POST ) || empty( $_POST['tvd_auth_check_user_key'] ) || empty( $this->tvd_interim_user_id ) ) {
        return;
    }

    /**
     * Problem: during the login POST, after login, the user does not seem to be actually available
     */
    $user_id = $this->tvd_interim_user_id;
    /**
     * This is used to correctly re-generate the nonce
     */
    $_COOKIE[ LOGGED_IN_COOKIE ] = $this->tvd_interim_login_cookie;
    $user_key                    = $this->get_user_editor_key( $user_id );

    wp_set_current_user( $user_id );

    if ( $user_key === $_POST['tvd_auth_check_user_key'] ) {
        /* pass data that we need after the login auth */
        $data = apply_filters( 'tvd_auth_check_data', array(
            'rest_nonce' => wp_create_nonce( 'wp_rest' ),
        ) );
        include 'handle-login.php';
    }
}

The above function is hooked to the WordPress built-in function with the same name which is login_footer. If we look closely, the above function will check the $interim_login variable. This indicates that the function will check if the user authenticates to the site using the interim-login process.

In general, $interim_login tells WordPress that the user's session has expired, prompts the user to log back in, and takes them back to the page they were on when the session expired.

So, for example, if the user is sitting in the dashboard all day and then gets back on and tries to edit something but their session expired, and they are no longer logged in, WordPress will call interim-login and allow the user to log back in and continue editing where they left off.

The only thing left to trigger the tvd_auth_check_data filter is to fetch the $user_key value that is generated from the $this->get_user_editor_key function. This value apparently can be fetched from wp-admin the dashboard since it will be generated in the auth_enqueue_scripts function:

public function __construct() {
    /**
     * Actions used for handling the interim login ( login via popup in Thrive Theme Dashboard )
     */
    add_action( 'login_footer', array( $this, 'login_footer' ) );
    add_action( 'set_logged_in_cookie', array( $this, 'set_logged_in_cookie' ), 10, 4 );

    add_action( 'admin_enqueue_scripts', array( __CLASS__, 'auth_enqueue_scripts' ) );
}

public static function auth_enqueue_scripts() {
    tve_dash_enqueue_script( 'tvd-auth-check', TVE_DASH_URL . '/inc/auth-check/auth-check.js' );

    wp_localize_script( 'tvd-auth-check', 'tvd_auth_check', array( 'userkey' => static::generate_editor_key() ) );
}

To summarize all of the codes, to fetch the nonce, an authenticated user with a Subscriber role needs to log in first and fetch the user key from the wp-admin dashboard page source. The user then needs to log out and proceed to perform the interim-login process with the supplied tvd_auth_check_user_key value. The tcb_nonce value then will be reflected in the HTTP response. From that, the user can request to the dismiss tooltip action and modify their wp_capabilities user meta value.

Note that this vulnerability can be triggered on a default theme installation without additional required configuration.

The patch

Since the main issue is an arbitrary user meta update, limiting the meta key that will be updated should be enough to fix the main issue. The patch can be seen below:

Additionally, to prevent lower-role users from accessing the editor Ajax action, the vendor decided to add access control to the handle function using the has_external_access function. The patch can be seen below :

Conclusion

Always implement proper permission and nonce validation on each function that processes or views sensitive data. The usage of nonce validation only, without a proper permission check could still lead to a broken access control if the user can fetch the checked nonce value.

To utilize the update_user_meta function, we need to implement a whitelist on which the meta key could be updated. It is also recommended to apply sanitization on the supplied meta value.

Timeline

03 July, 2023We found the vulnerability and reached out to the plugin vendor.
15 August, 2023We notified the vendor that the reported issue were not patched sufficiently in version 3.21.2.
21 September, 2023The vendor notified us about the complete patches, but patched versions were not provided.
14 November, 2023Vulnerability entries published with no patched versions (The vendor still not providing the patched version to validate).
15 November, 2023The vendor reach back to us with the patched version. We validated the patch and updated the vulnerability entries.
29 November, 2023Security advisory article publicly released.

Help us make the Internet a safer place

Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.

  • If you're a plugin developer, join our mVDP program, making it easier to report, manage, and address vulnerabilities in your software.
  • If you're a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.

The latest in Security Advisories

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