4 Sep 2024

WordPress Plugin Security Review: Profile Builder

For our 44th security review of a WordPress plugin based on the voting of our customers, we reviewed the plugin Profile Builder.

If you are not yet a customer of the service, once you sign up for the service as a paying customer, you can start suggesting and voting on plugins to get security reviews. For those already using the service that haven’t already suggested and voted for plugins to receive a review, you can start doing that here. You can use our tool for doing limited automated security checks of plugins to see if plugins you are using have possible issues that would make them good candidates to get a review. You can also order a review of a plugin separately from our service.

The review was done on version 3.12.2 of Profile Builder. We checked for the following issues during it as part of our standard review:

  • Insecure file upload handling (this is the cause of the most exploited type of vulnerability, arbitrary file upload)
  • Deserialization of untrusted data
  • Security issues with functions accessible through WordPress’ AJAX functionality (those have and continued to be a common source of disclosed vulnerabilities)
  • Security issues with functions accessible through WordPress’ REST API (those have started to be a source of disclosed vulnerabilities)
  • Persistent cross-site scripting (XSS) vulnerabilities in the frontend portions of the plugin and in the admin portions accessible to users with the Author role or below
  • Cross-site request forgery (CSRF) vulnerabilities in the admin portion of the plugin
  • SQL injection vulnerabilities (the code that handles requests to the database)
  • Reflected cross-site scripting (XSS) vulnerabilities
  • Security issues with functions accessible through any of the plugin’s shortcodes
  • Security issues with functions accessible through any of the plugin’s blocks
  • Security issues with functions accessible through the admin_action action
  • Security issues with functions accessible through the admin_init action
  • Security issues with functions accessible through the admin_post action
  • Security issues with import/export functionality
  • Security issues with usage of the is_admin() function
  • Security issues with usage of the add_option(), delete_option(), and update_option() functions
  • Security issues with usage of the update_user_meta() and wp_update_user() functions
  • Security with usage of determine_current_user filter
  • Security issues with usage of the wp_set_current_user(), wp_set_auth_cookie() and wc_set_customer_auth_cookie() functions
  • Security issues with usage of the reset_password() and wp_set_password() functions
  • Security issues with usage of the extract() function
  • Lack of IP address validation
  • Proper usage of sanitize_callback when using register_setting() to register settings.
  • CSV injection
  • Host header injection vulnerabilities
  • Lack of protection against unintended direct access of PHP files
  • Insecure and unwarranted requests to third-party websites
  • Any additional possible issues identified by our Plugin Security Checker

Results

We found the plugin contained several vulnerabilities and several places where security could be improved, which are detailed below.

We notified of developer of the issues we found on August 21. They got back to us the next day, stating they were working on things. Two days ago they released version 3.12.3, to address the issues. The developer was less than forthcoming about that, as the changelog entry simply reads “Fix: Security issues.” We found they mostly resolved the issues.

Featured Image Deletion

In the AJAX accessible function ajax_remove_thumbnail() in the file /add-ons-free/user-profile-picture/metronet-profile-picture.php there is vulnerability, as it allows deleting the featured image for arbitrary posts with this code:

464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
public function ajax_remove_thumbnail() {
	if ( ! current_user_can( 'upload_files' ) ) {
		die( '' );
	}
	$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
	$user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : 0;
	if ( 0 === $post_id || 0 === $user_id ) {
		die( '' );
	}
	check_ajax_referer( "mt-update-post_$user_id" );
 
	$thumb_html  = '<a style="display:block" href="#" class="mpp_add_media default-image">';
	$thumb_html .= sprintf( '<img style="display:block" src="%s" width="150" height="150" title="%s" />', self::get_plugin_url( 'img/mystery.png' ), esc_attr__( 'Upload or Change Profile Picture', 'profile-builder' ) );
	$thumb_html .= sprintf( '<div id="metronet-click-edit">%s</div>', esc_html__( 'Click to Edit', 'profile-builder' ) );
	$thumb_html .= '</a>';
 
	// Save user meta and update thumbnail.
	update_user_option( $user_id, 'metronet_image_id', 0 );
	delete_post_meta( $post_id, '_thumbnail_id' );

(Intended functionality related to the code doesn’t work in normal circumstances because of a failure to pass a nonce.)

The user does need the upload_files capability to access this, so normally only user with the Author role and above could exploit this. The User Profile Picture addon needs to be enabled as well to exploit this.

No change was made to address this. We noted that with the developer.

Cross-Site Request Forgery (CSRF) Vulnerabilities

There was a lack of a nonce check to prevent CSRF in the function dismiss_notification() in the file /admin/register-version.php. The same issue occurred with the function of the same name in the files /admin/review.php and /assets/lib/class_notices.php. That also occured with the function dismiss_setup_wizard_newsletter_subscribe() in the file /admin/setup-wizard.php. That was resolved by adding the missing nonce check like this:

149
150
151
152
function dismiss_notification() {
 
	if( empty( $_GET['_wpnonce'] ) || !wp_verify_nonce( sanitize_text_field( $_GET['_wpnonce'] ), 'wppb_license_notice_dismiss' ) )
		return;

The same issue occurred with the function rest_api_put_profile() in the file /add-ons-free/user-profile-picture/metronet-profile-picture.php. That is called through the REST API and should have had a permission_callback for that checked for the relevant permission to access that functionality, which would have caused the nonce check to happen automatically. That was resolved by replacing the permission_callback that didn’t check for a permission:

1029
'permission_callback' => '__return_true',

With a check for a permission:

1031
1032
1033
'permission_callback' => function( $request ){
	return current_user_can( 'manage_options' );
}

Missing Capability Checks

The following AJAX accessible functions in the plugin were lacking a capability check to limit access to them to only the intended users:

  • wppb_le_delete_all_fields_callback() in /add-ons-free/labels-edit/labels-edit.php
  • wppb_get_dashboard_stats() in /admin/dashboard.php
  • wppb_get_unconfirmed_email_number() in /features/email-confirmation/email-confirmation.php
  • wck_show_update_form() and wck_refresh_list() in /assets/lib/wck-api/wordpress-creation-kit.php

There was a nonce check, which would normally do the equivalent of that, but as the documentation for that states, it shouldn’t be relied on for that:

Nonces should never be relied on for authentication, authorization, or access control

That was resolved by adding a capability check using current_user_can() like this:

390
391
if( !current_user_can( 'manage_options' ) )
	die();

There was with the same issue with these admin_init accessible functions (even those not logged in to WordPress can access admin_init accessible functions):

  • activate_license() and deactivate_license() in /update/class-edd-sl-plugin-updater.php
  • process_optin_actions() in /admin/plugin-optin/class-plugin-optin.php.
  • wppb_pbie_export_our_json() in /add-ons-free/import-export/pbie-export.php
  • wppb_le_export() in /add-ons-free/labels-edit/labels-edit.php
  • wppb_form_design_new_styles_notification() in /admin/admin-functions.php
  • save_data() in /admin/setup-wizard.php
  • set_existing_user_pages() in /admin/setup-wizard.php

Unprepared SQL Statements

We found three SQL statements that were not prepared statements, which would prevent SQL injection from being possible. Those occurred in the function wppb_get_user_map_markers() in the file /admin/manage-fields.php, the functionprepare_items() in the file /features/email-confirmation/class-email-confirmation.php, and the function wppb_ec_replace_password() in the file /features/email-customizer/email-customizer.php. That was resolved by preparing them. Here is one of those before being prepared:

1607
$results = $wpdb->get_results( "SELECT meta_value, meta_key FROM {$wpdb->usermeta} WHERE user_id={$user_id} AND meta_key LIKE '%{$meta_name_underlined}%'", ARRAY_N );

And after being changed:

1607
$results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value, meta_key FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key LIKE '%s'", $user_id, '%' . $meta_name_underlined . '%' ), ARRAY_N );

Misuse of esc_url_raw()

In the function wppb_edit_profile_select_user_to_edit() in the file /front-end/class-formbuilder.php, the WordPress escaping function esc_url_raw() was being misused. According to its documentation, it should only be used to sanitize a URL for database or redirect usage. But it was being used to output a value on a page.

776
<option value="<?php echo  esc_url_raw( add_query_arg( array( 'edit_user' => $user->ID ) ) ); ?>" <?php selected( $selected, $user->ID ); ?>>

That was addressed by using esc_url() instead:

776
<option value="<?php echo  esc_url( add_query_arg( array( 'edit_user' => $user->ID ) ) ); ?>" <?php selected( $selected, $user->ID ); ?>>

Missing URL Escaping

The functions wppb_build_redirect() in the file /features/functions.php and the function wppb_form_design_new_styles_notification() in the file /admin/admin-functions.php, didn’t escape the URL being output. That was resolved by using esc_url() or wp_nonce_url() to handle the escaping.

PHP Function filter_input() Use Without a Filter

In the function save_user_profile() in the file /add-ons-free/user-profile-picture/metronet-profile-picture.php the PHP function filter_input() was used without a filter, so it didn’t do any filtering:

1374
$user_avatar = filter_input( INPUT_POST, 'metronet-user-avatar' );

That was resolved by specifying a filter:

1374
$user_avatar = filter_input( INPUT_POST, 'metronet-user-avatar', FILTER_SANITIZE_STRING );

extract() Usage

In multiple locations in the plugin, it was using the extract() function on user input. The documentation for that function warns that it shouldn’t be used for that, “Do not use extract() on untrusted data, like user input.” WordPress’ own coding standard says it shouldn’t be used. One example of that occurred with shortcode attributes in the file /front-end/edit-profile.php:

89
extract( shortcode_atts( array( 'form_name' => 'unspecified', 'redirect_url' => '', 'redirect_priority' => 'normal' ), $atts, 'wppb-edit-profile' ) );

The extraction was removed there, but not removed elsewhere.

A better way to handle code like that would be to set individual variables to the value of the user input and to sanitize/validate the input when doing that.

Lack of Protection Against Direct Access to PHP Files

Some of the plugin’s .php files do not have code at the beginning of the files to restrict direct access to them. We didn’t see anything that could be exploited in the files without the restriction in place, but restricting access to them would ensure that there isn’t any issue with that. That protection has now been added to more, but not all the files.


Plugin Security Scorecard Grade for Profile Builder

Checked on October 13, 2024
C

See issues causing the plugin to get less than A+ grade

Leave a Reply

Your email address will not be published.