Critical vulnerability in WordPress Ultimate GDPR and CCPA Compliance Toolkit plugin.

The WordPress Ultimate GDPR & CCPA Compliance Toolkit plugin, which has 6,000+ sales on Envato Market, was prone to a critical unauthenticated settings import and export vulnerability affecting version 2.4 and below that could allow an attacker to redirect traffic to a malicious site among other issues.

Unauthenticated Settings Import & Export

In the “ct-ultimate-gdpr/includes/controller/controller-admin.php” script, line 36, the plugin loads the admin_actions function if the WordPress is_admin function returns true:

public function __construct() {

   $this->register_menu_pages();
   $this->register_option_fields();
   $this->register_styles();

   if ( is_admin() ) {
      $this->admin_actions();
   }

The is_admin function doesn’t guarantee that the user is logged-in or is an administrator. Instead, it will always return true whenever a user, authenticated or not, accesses some scripts inside the “/wp-admin/” folder such as “wp-admin/index.php” or “wp-admin/admin-ajax.php” for instance.
The admin_actions function is used to load four different functions depending on the $_POST variable:

private function admin_actions() {

   if ( $this->is_request_export_settings() ) {
      add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'export_settings' ) );
   }

   if ( $this->is_request_import_settings() ) {
      add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'import_settings' ) );
   }

   if ( $this->is_request_export_services() ) {
      add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'export_services' ) );
   }

   if ( $this->is_request_import_services() ) {
      add_action( 'ct_ultimate_gdpr_after_controllers_registered', array( $this, 'import_services' ) );
   }

}

/**
 * @return bool
 */
private function is_request_export_settings() {

   if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-export', $_POST ) ) {
      return true;
   }

   return false;
}

/**
 * @return bool
 */
private function is_request_import_settings() {

   if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-import', $_POST ) ) {
      return true;
   }

   return false;

}

/**
 * @return bool
 */
private function is_request_export_services() {

   if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-export-services', $_POST ) ) {
      return true;
   }

   return false;
}

/**
 * @return bool
 */
private function is_request_import_services() {

   if ( ct_ultimate_gdpr_get_value( 'ct-ultimate-gdpr-import-services', $_POST ) ) {
      return true;
   }

   return false;

}

The import_settings function, used to import all settings from a JSON-encoded file, doesn’t check for user capabilities and lacks a security nonce:

/**
 * Import from json file
 */
public function import_settings() {

   $import_file = isset( $_FILES['ct-ultimate-gdpr-settings-file']['tmp_name'] ) ? $_FILES['ct-ultimate-gdpr-settings-file']['tmp_name'] : '';

   if ( empty( $import_file ) ) {
      $this->view_options['notices'] = array( esc_html__( 'Please upload a file to import', 'ct-ultimate-gdpr' ) );

      return;
   }

   // Retrieve the settings from the file and convert the json object to an array.
   $settings = (array) json_decode( file_get_contents( $import_file ), true );

   if ( empty( $settings ) ) {
      $this->view_options['notices'] = array( esc_html__( 'No options were imported', 'ct-ultimate-gdpr' ) );

      return;
   }

   $updated = false;

   foreach ( $settings as $id => $options ) {

      $check_id = CT_Ultimate_GDPR::instance()->get_controller_by_id( $id );

      if ( $check_id ) {

         // update controller options
         $updated = $updated || update_option( $id, $options );

      }

   }

   $this->view_options['notices'] = $updated ?
      array( esc_html__( 'Settings imported successfully', 'ct-ultimate-gdpr' ) ) :
      array( esc_html__( 'Settings were not imported. Please check the import file.', 'ct-ultimate-gdpr' ) );

}

An unauthenticated user can change the plugin’s settings and redirect all traffic to an external malicious website.
This vulnerability is particularly damaging because the attacker can even choose which traffic should be redirected: guest users and/or authenticated users and/or admin users.

The redirection is performed via the Location header:

$ curl 'http://example.com/' -I
HTTP/1.1 302 Found
Server: nginx/1.18.0
Date: Thu, 21 Jan 2021 12:42:46 GMT
X-Redirect-By: WordPress
Location: https://evilsite.com/

The export_settings function, used to export the plugin’s settings, is too accessible to anyone:

public function export_settings() {

   $controllers = CT_Ultimate_GDPR::instance()->get_controllers();

   $settings = array();

   /** @var CT_Ultimate_GDPR_Controller_Abstract $controller */
   foreach ( $controllers as $controller ) {

      $options = $controller->get_options_to_export();
      if ( $options ) {
         $settings[ $controller->get_id() ] = $options;
      }

   }

   nocache_headers();
   header( 'Content-Type: application/json; charset=utf-8' );
   header( 'Content-Disposition: attachment; filename=ct-ultimate-gdpr-settings-export-' . date( 'm-d-Y' ) . '.json' );
   header( "Expires: 0" );

   echo json_encode( $settings );
   exit;

}

An unauthenticated user can export all settings, modify the data and reinject it using the previous vulnerability.

Additional Issues

The export_services and import_services functions used to import/export services from/to the Service Manager are also accessible to any user, authenticated or not.

Recommendations

Update immediately if you have version 2.4 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.

Timeline

The vulnerability was reported to the Envato team on January 22. A new version 2.5 (which restricts access to the vulnerable code to admin users only but still lacks a security nonce in the two import functions) was released on January 28.

Stay informed about the latest vulnerabilities