Critical vulnerability fixed in WordPress MStore API plugin.

The WordPress MStore API plugin, which has 1,000+ active installations, fixed a critical vulnerability affecting version 2.1.5 and below that could allow an unauthenticated user to create or edit administrator accounts.

In the “mstore-api/controllers/FlutterUser.php” script, the plugin registers several custom endpoints via the WordPress REST API, among them the register and update_user_profile routes which are accessible to any user, logged-in or not:

public function __construct() {
   $this->namespace     = '/api/flutter_user';
}
 
public function register_routes() {
   register_rest_route( $this->namespace, '/register', array(
      array(
         'methods'   => 'POST',
         'callback'  => array( $this, 'register' )
      ),
   ));
...
...
   register_rest_route( $this->namespace, '/update_user_profile', array(
      array(
         'methods'   => 'POST',
         'callback'  => array( $this, 'update_user_profile' )
      ),
   ));
}

The register method can be used by any unauthenticated user to create an account on the blog:

public function register()
{
   $json = file_get_contents('php://input');
   $params = json_decode($json);
   $usernameReq = $params->username;
   $emailReq = $params->email;
   $secondsReq = $params->seconds;
   $nonceReq = $params->nonce;
   $roleReq = $params->role;
   $userPassReq = $params->user_pass;
   $userLoginReq = $params->user_login;
   $userEmailReq = $params->user_email;
   $notifyReq = $params->notify;
   
   $username = sanitize_user($usernameReq);

   $email = sanitize_email($emailReq);

   if ($secondsReq) {
      $seconds = (int) $secondsReq;
   } else {
      $seconds = 120960000;
   }
   if (!validate_username($username)) {
      return parent::sendError("invalid_username","Username is invalid.", 400);
   } elseif (username_exists($username)) {
      return parent::sendError("existed_username","Username already exists.", 400);
   } else {
      if (!is_email($email)) {
         return parent::sendError("invalid_email","E-mail address is invalid.", 400);
      } elseif (email_exists($email)) {
         return parent::sendError("existed_email","E-mail address is already in use.", 400);
      } else {
         if (!$userPassReq) {
            $params->user_pass = wp_generate_password();
         }

         $allowed_params = array('user_login', 'user_email', 'user_pass', 'display_name', 'user_nicename', 'user_url', 'nickname', 'first_name',
            'last_name', 'description', 'rich_editing', 'user_registered', 'role', 'jabber', 'aim', 'yim',
            'comment_shortcuts', 'admin_color', 'use_ssl', 'show_admin_bar_front',
         );

         $dataRequest = $params;

         foreach ($dataRequest as $field => $value) {
            if (in_array($field, $allowed_params)) {
               $user[$field] = trim(sanitize_text_field($value));
            }
         }
         
         $user['role'] = $roleReq ? sanitize_text_field($roleReq) : get_option('default_role');
         $user_id = wp_insert_user($user);

         if(is_wp_error($user_id)){
            return parent::sendError($user_id->get_error_code(),$user_id->get_error_message(), 400);
         }

         // if ($userPassReq && $notifyReq && $notifyReq == 'no') {
         //    $notify = '';
         // } elseif ($notifyReq && $notifyReq != 'no') {
         //    $notify = $notifyReq;
         // }

         // if ($user_id) {
         //    wp_new_user_notification($user_id, '', $notify);
         // }
      }
   }

   $expiration = time() + apply_filters('auth_cookie_expiration', $seconds, $user_id, true);
   $cookie = wp_generate_auth_cookie($user_id, $expiration, 'logged_in');

   return array(
      "cookie" => $cookie,
      "user_id" => $user_id,
   );
}

Because it doesn’t check if the blog accepts user accounts creation and it allows the user to send their own role, an unauthenticated attacker can create an administrator account:

After creating the account, the plugin will also call the wp_generate_auth_cookie function and return the new administrator’s authentication cookie, allowing the attacker to get administrator privileges without having to log in:

{"cookie":"foo|1581746054|JlQOeEqaFwoHkVoUv6FnAsdD6swfIr6zbizs2tToWnY|aa12883061ade5357176a11fb96980dd955b4d5aa85c23b5c31a9e664a588bd7","user_id":11}

The update_user_profile function allows the user to update their account information:

function update_user_profile() {
   global $json_api;
   if ($_SERVER['REQUEST_METHOD'] === 'POST') {
      $json = file_get_contents('php://input');
      $params = json_decode($json);
   } else {
      echo 'Not found url. :))';
   }
   if (!$params->user_id) {
      $json_api->error("You must include a 'user_id' var in your request.");
   }
   $user_update = array( 'ID' => $params->user_id);
   if ($params->user_pass) {
      $user_update['user_pass'] = $params->user_pass;
   }
   if ($params->user_nicename) {
      $user_update['user_nicename'] = $params->user_nicename;
   }
   if ($params->user_email) {
      $user_update['user_email'] = $params->user_email;
   }
   if ($params->user_url) {
      $user_update['user_url'] = $params->user_url;
   }
   if ($params->display_name) {
      $user_update['display_name'] = $params->display_name;
   }
   $user_data = wp_update_user($user_update);
 
   if ( is_wp_error( $user_data ) ) {
     // There was an error; possibly this user doesn't exist.
      echo 'Error.';
   }
   return get_userdata($params->user_id);
}

It doesn’t check if the user is authenticated and updating their own account. An unauthenticated user could modify any account on the blog such as the administrator account’s password or email address.

Additional Issues

MStore API registers several custom endpoints via the JSON API plugin, which hasn’t been updated for five years and was removed from wordpress.org on August 7, 2019 due to security concern. The access to that API is disabled by default and it should not be activated at all with the MStore API plugin because it would lead to critical security issues.
It performs the same actions as described above, the only differences being the routes name (/api/mstore_user/update_user_profile/ and /api/mstore_user/register/) and the fact that the register function requires that the user_can_register option be enabled on the blog.

Timeline

The vulnerabilities were discovered and reported to the wordpress.org team on February 19, 2020. A new version 2.1.6 was released on February 21, 2020.

Recommendations

Update as soon as possible if you have version 2.1.5 or below installed.
If you are using our web application firewall for WordPress, NinjaFirewall WP Edition (free) and NinjaFirewall WP+ Edition (premium), you are protected against this vulnerability.

Stay informed about the latest vulnerabilities in WordPress plugins and themes: @nintechnet