WordPress Brizy Page Builder plugin fixed critical vulnerabilities.

The WordPress Brizy Page Builder plugin (60,000+ active installations) fixed a broken access control vulnerability affecting version 1.0.125 and below that could allow any authenticated user to gain full access to the editor.

Role Manager

Like many similar page builder plugins, Brizy uses its own role manager:

It relies on its two capabilities (brizy_edit_whole_page and brizy_edit_content_only) to allow or restrict access to the editor. There’s no other safeguard, except a couple of security nonces used to prevent CSRF attacks. However, all nonces and the role manager can be bypassed by any authenticated user, allowing them to interact with most of the 60+ AJAX actions.

Broken Access Control

Throughout its code, Brizy checks if the user is allowed to access the editor and its functions by calling is_user_allowed(), for instance in the initializeApiActions() function located in the ‘brizy/editor/api.php’ script:

protected function initializeApiActions() {
   if ( Brizy_Editor::is_user_allowed() ) {
      add_action( 'wp_ajax_' . self::AJAX_REMOVE_LOCK, array( $this, 'removeProjectLock' ) );
      add_action( 'wp_ajax_' . self::AJAX_HEARTBEAT, array( $this, 'heartbeat' ) );
      add_action( 'wp_ajax_' . self::AJAX_TAKE_OVER, array( $this, 'takeOver' ) );
      add_action( 'wp_ajax_' . self::AJAX_GET, array( $this, 'get_item' ) );
      add_action( 'wp_ajax_' . self::AJAX_GET_POST_INFO, array( $this, 'get_post_info' ) );
      add_action( 'wp_ajax_' . self::AJAX_UPDATE, array( $this, 'update_item' ) );
      add_action( 'wp_ajax_' . self::AJAX_GET_PROJECT, array( $this, 'get_project' ) );
      add_action( 'wp_ajax_' . self::AJAX_SET_PROJECT, array( $this, 'set_project' ) );
      ...
      ...

The is_user_allowed() function is located in the ‘brizy/editor.php’ script:

public static function is_user_allowed() {

   if ( ! is_user_logged_in() ) {
      return false;
   }

   if ( self::is_administrator() ) {
      return true;
   }

   if ( is_null( self::$is_allowed_for_current_user ) ) {
      self::$is_allowed_for_current_user =
         (
            current_user_can( Brizy_Admin_Capabilities::CAP_EDIT_WHOLE_PAGE ) ||
            current_user_can( Brizy_Admin_Capabilities::CAP_EDIT_CONTENT_ONLY )
         );
   }

   return self::$is_allowed_for_current_user;
}

It checks if the user is logged in and has the right capability, or if it is an admin by calling the is_administrator() function found in the same script:

public static function is_administrator() {

   if ( ! is_user_logged_in() ) {
      return false;
   }

   return is_admin() || is_super_admin();
}

The function will check again is the user is logged in and will return the value of either the is_admin() or is_super_admin() functions. However, if is_super_admin() will indeed check if the user is an administrator (single site) or superadmin (multisite), the is_admin() function will only check if the user is accessing a page from the backend, hence the function will always return true when called by a logged in user.

Security Nonces

Approximately 40 AJAX actions use the verifyNonce() function to check the security nonce:

$this->verifyNonce( self::nonce );

However, the nonce check in that function is commented out:

protected function verifyNonce( $action ) {

   $version = $this->param( 'version' );
   if ( $version !== BRIZY_EDITOR_VERSION ) {
      Brizy_Logger::instance()->critical( 'Request with invalid version',
         [
            'editorVersion'   => BRIZY_EDITOR_VERSION,
            'providedVersion' => $version
         ] );

      $this->error( 400, "Invalid editor version. Please refresh the page and try again" );
   }
//   if ( ! wp_verify_nonce( $this->getRequestNonce(), $action ) ) {
//      Brizy_Logger::instance()->error( 'Invalid request nonce', $_REQUEST );
//      $this->error( 400, "Bad request" );
//   }
}

Most other AJAX actions rely on another nonce, Brizy_Editor_API::nonce in the authorize() function:

private function authorize() {
   if ( ! wp_verify_nonce( $_REQUEST['hash'], Brizy_Editor_API::nonce ) ) {
      wp_send_json_error( array( 'code' => 400, 'message' => 'Bad request' ), 400 );
   }
}

The nonce is populated by the action_register_static() function in the ‘brizy/admin/main.php’ script:

// enqueue admin scripts
add_action( 'admin_enqueue_scripts', array( $this, 'action_register_static' ) );
...
...
public function action_register_static() {
   ...
   ...
   wp_localize_script(
      Brizy_Editor::get()->get_slug() . '-admin-js',
      'Brizy_Admin_Data',
      array(
         'url'           => admin_url( 'admin-ajax.php' ),
         'pluginUrl'     => BRIZY_PLUGIN_URL,
         'ruleApiHash'   => wp_create_nonce( Brizy_Admin_Rules_Api::nonce ),
   ...
   ...

Because the function is loaded via the admin_enqueue_scripts hook, the nonce is accessible to any logged in user in the source of every HTML page in the backend:

A low-privileged user, such as a subscriber, could interact with all AJAX functions of the plugin.

Due to the large number of actions, I’m only reviewing a few of them below:

/brizy/editor/api.php

  • brizy_update_item: This action can be used by any authenticated user to create a new page or post (when used with brizy_set_project action), or to update any existing page/post. The attacker can inject any content, including JavaScript code.
  • brizy_get_post_info: This action can be used by any authenticated user to view any post/page, regardless of their status, i.e., private or even password-protected posts.
  • brizy_set_featured_image and brizy_remove_featured_image: These actions can be used by any authenticated user to add/delete the post featured image.

/brizy/editor/forms/api.php

  • brizy_get_form and brizy_get_integration: Either action will return all info about the corresponding form (its ID can be found in the source of the HTML page by any visitor), including the user Gmail account login and password, the user SMTP server credentials, as well as any of the secret integrations keys used by other applications and services in the pro version (Mailchimp etc).
  • brizy_update_integration: This action allows the attacker to change the form configuration, including the recipient address in order to hijack the form. All submitted messages will be redirected (To:) or forwarded (Cc:, Bcc:) to them.

Additional scripts with AJAX actions that can be accessed by any logged-in user:

  • /brizy/admin/fonts/api.php
  • /brizy/admin/rules/api.php
  • /brizy/admin/blocks/api.php
  • /brizy/editor/accounts/api.php

Pro version (slug: brizy-pro):

  • /brizy-pro/forms/api-extender.php

Timeline

The vulnerability was reported to the authors on May 20th, 2020 and a new version 1.0.125 was released on June 2nd, 2020. However, because all vulnerabilities were still exploitable in that version, the authors were contacted the same day again and a new version 1.0.126 was released on June 3rd, 2020.

Recommendations

Upgrade immediately if you have version 1.0.125 or below. 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