Sixteen WooCommerce product add-ons plugins fixed a broken access control vulnerability that could allow customers to take over the website and its database.
Vulnerable Plugins
All plugins are available on the developers’ website and several of them on CodeCanyon as well.
The following plugins and versions are vulnerable:
- Product Filter for WooCommerce (
prdctfltr
) <=8.1.1. Fixed in 8.2.0. - Improved Product Options for WooCommerce (
improved-variable-product-attributes
) <=5.1.1. Fixed in 5.3.0. - Improved Sale Badges for WooCommerce (
improved-sale-badges
) <=4.3.1. Fixed in 4.4.0. - Share, Print and PDF Products for WooCommerce (
share-print-pdf-woocommerce
) <=2.7.1. Fixed in 2.8.0 - Product Loops for WooCommerce (
product-loops
) <=1.6.1. Fixed in 1.7.0. - XforWooCommerce (
xforwoocommerce
) <=1.6.4. Fixed in 1.7.0. - Package Quantity Discount (
package-quantity-xforwc
) <=1.1.1. Fixed in 1.2.0. - Price Commander for WooCommerce (
price-commander-xforwc
) <=1.2.1. Fixed in 1.3.0. - Comment and Review Spam Control for WooCommerce (
spam-control-xforwc
) <=1.4.1. Fixed in 1.5.0. - Add Product Tabs for WooCommerce (
add-tabs-xforwc
) <=1.4.1. Fixed in 1.5.0. - Autopilot SEO for WooCommerce (
seo-for-woocommerce
) <=1.5.1. Fixed in 1.6.0. - Floating Cart (
floating-cart-xforwc
) <=1.2.1. Fixed in 1.3.0. - Live Search for WooCommerce (
live-search-xforwc
) <=1.3.1. Fixed in 2.1.0. - Bulk Add to Cart for WooCommerce (
bulk-add-to-cart-xforwc
) <=1.2.1. Fixed in 1.3.0. - Live Product Editor for WooCommerce (
woocommerce-frontend-shop-manager
) <=4.6.1. Fixed in 4.7.0. - Warranties and Returns for WooCommerce (
woocommerce-warranties-and-returns
) <=5.2.1. Fixed in 5.3.0.
Because the vulnerability and the way to exploit it is identical in all 16 plugins, we will review in this advisory the Product Filter for WooCommerce plugin, which has 16,000+ sales on CodeCanyon.
Authenticated Arbitrary WordPress Options Change, Read and Deletion / Authenticated User Enumeration / Authenticated Plugin Settings Change, Import and Export
CVSS v3.1: 8.8 (High)
All plugins offer the possibility to export, import, backup, restore, reset and save their options in the admin backend:
They rely on the same svx_ajax_factory
AJAX action found in the “includes/svx-settings/svx-settings.php” script:
add_action( 'wp_ajax_svx_ajax_factory', array( $this, 'ajax_factory' ) );
The svx_ajax_factory
action loads the ajax_factory
function. Although the AJAX action requires authentication, it is important to note that all 16 plugins are WooCommerce product add-ons, therefore an unauthenticated user could simply sign up for a WooCommerce customer account and, using the authentication cookie, access that function.
The ajax_factory
function is found in the same script:
public function ajax_factory() { $opt = array( 'success' => true ); if ( !isset( $_POST['svx']['type'] ) ) { SevenVX()->ajax_die($opt); } if ( apply_filters( 'svx_can_you_save', false ) ) { $this->ajax_die($opt); } switch( $_POST['svx']['type'] ) { case 'get_control_options' : $set = explode( ':', $_POST['svx']['settings'] ); switch( $set[1] ) { case 'image_sizes' : $image_array = array(); $image_sizes = get_intermediate_image_sizes(); foreach ( $image_sizes as $image_size ) { $image_array[$image_size] = $image_size; } wp_send_json( $image_array ); exit; break; case 'wp_options' : wp_send_json( get_option( substr( $_POST['svx']['settings'], 16 ) ) ); exit; break; case 'users' : $return = array(); $users = get_users( array( 'fields' => array( 'id', 'display_name' ) ) ); foreach ( $users as $user ) { $return[$user->id] = $user->display_name; } wp_send_json( $return ); exit; break; case 'product_attributes' : wp_send_json( SevenVX()->_attributes_get_alt() ); exit; break; case 'product_taxonomies' : wp_send_json( SevenVX()->_taxonomies_get() ); exit; break; case 'product_types' : wp_send_json( SevenVX()->_types_get() ); exit; break; case 'taxonomy' : wp_send_json( SevenVX()->_terms_get( $set, 'select' ) ); exit; break; case 'terms' : wp_send_json( SevenVX()->_terms_get( $set, 'terms' ) ); exit; break; default : SevenVX()->ajax_die($opt); break; } break; case 'save' : $slc = isset( $_POST['svx']['delete'] ) && is_array( $_POST['svx']['delete'] ) ? $_POST['svx']['delete'] : array(); if ( !empty( $slc ) ) { foreach( $slc as $k => $v ) { delete_option( $v ); } } $sld = isset( $_POST['svx']['solids'] ) && is_array( $_POST['svx']['solids'] ) ? $_POST['svx']['solids'] : array(); if ( !empty( $sld ) ) { foreach( $sld as $k => $v ) { $val = isset( $v['val'] ) && !empty( $v['val'] ) ? $v['val'] : false; if ( !is_array( $val ) ) { $val = array(); } $std = get_option( $k, array() ); if ( !is_array( $std ) ) { $std = array(); } if ( empty( $val ) ) { update_option( $k, '', false ); } else { update_option( $k, $val, false ); } } } $stg = isset( $_POST['svx']['settings'] ) && is_array( $_POST['svx']['settings'] ) ? $_POST['svx']['settings'] : array(); foreach( $stg as $k => $v ) { if ( isset( $v['autoload'] ) ) { if ( $v['autoload'] == 'true' ) { $opt['auto'][$k] = $v['val']; } else if ( $v['autoload'] == 'false' ) { $opt['std'][$k] = isset( $v['val'] ) ? $v['val'] : false; } } } $opt = apply_filters( 'svx_ajax_save_settings', $opt ); if ( isset( $opt['std'] ) && !empty( $opt['std'] ) && is_array( $opt['std'] ) ) { update_option( 'svx_settings_' . $_POST['svx']['plugin'], array_merge( get_option( 'svx_settings_' . $_POST['svx']['plugin'], array() ), $opt['std'] ), false ); } $less = isset( $_POST['svx']['less'] ) && is_array( $_POST['svx']['less'] ) ? $_POST['svx']['less'] : array(); if ( !empty( $less['length'] ) && $less['length'] > 0 ) { $option = isset( $less['option'] ) ? $less['option'] : false; if ( $option !== false ) { unset( $less['option'] ); if ( isset( $less['solids'] ) ) { $solids = $less['solids']; if ( isset( $solids['name'] ) ) { $presets = $opt['std'][$solids['name']]; foreach( $presets as $b => $j ) { $preset = apply_filters( 'svx_before_solid' . $solids['solid'], get_option( $solids['solid'] . sanitize_title( $b ), array() ) ); if ( isset( $preset['name'] ) ) { foreach( $solids['options'] as $n ) { if ( isset( $preset[$n] ) ) { switch ( $n ) { case 'name' : $less[$n . 's'] .= $less[$n . 's'] == '' ? sanitize_title( $b ) : ',' . sanitize_title( $b ); break; default : $less[$n . 's'] .= $less[$n . 's'] == '' ? $preset[$n] : ',' . $preset[$n]; break; } } } } } } $less['url'] = '~"' . $less['solids']['url'] . '"'; unset( $less['solids'] ); } $compiled = self::compile_less( $solids, $less ); } } if ( isset( $compiled ) ) { $opt['auto'][$option] = $compiled; } if ( isset( $opt['auto'] ) && !empty( $opt['auto'] ) && is_array( $opt['auto'] ) ) { $opt['auto'] = array_merge( get_option( 'svx_autoload', array() ), $opt['auto'] ); } $opt = apply_filters( 'svx_ajax_save_settings_auto', $opt ); if ( !empty( $opt['auto'] ) ) { update_option( 'svx_autoload', $opt['auto'], true ); } do_action( 'svx_ajax_saved_settings_' . $_POST['svx']['plugin'], $opt ); wp_send_json( array( 'success' => true ) ); exit; break; case 'export' : $stg = isset( $_POST['svx']['settings'] ) && is_array( $_POST['svx']['settings'] ) ? $_POST['svx']['settings'] : array(); if ( isset( $stg['auto'] ) && !empty( $stg['auto'] ) && is_array( $stg['auto'] ) ) { $backup_auto = get_option( 'svx_autoload', array() ); foreach( $stg['auto'] as $k ) { if ( isset( $backup_auto[$k] ) ) { $exp['auto'][$k] = $backup_auto[$k]; } } } if ( isset( $stg['std'] ) && !empty( $stg['std'] ) && is_array( $stg['std'] ) ) { $exp['std'] = get_option( 'svx_settings_' . $_POST['svx']['plugin'], array() ); } if ( isset( $stg['solids'] ) && !empty( $stg['solids'] ) && is_array( $stg['solids'] ) ) { foreach( $stg['solids'] as $k ) { $exp['solids'][$k] = get_option( $k ); } } wp_send_json( $this->get_for_options( $exp ) ); exit; break; case 'import' : $stg = isset( $_POST['svx']['settings'] ) ? $_POST['svx']['settings'] : ''; if ( $stg !== '' ) { $opt = $this->get_for_options( json_decode( stripslashes( $stg ), true ) ); $opt = apply_filters( 'svx_ajax_save_settings', $opt ); if ( isset( $opt['auto'] ) && !empty( $opt['auto'] ) && is_array( $opt['auto'] ) ) { $opt['auto'] = array_merge( get_option( 'svx_autoload', array() ), $opt['auto'] ); update_option( 'svx_autoload', $opt['auto'], true ); } if ( isset( $opt['std'] ) && !empty( $opt['std'] ) && is_array( $opt['std'] ) ) { update_option( 'svx_settings_' . $_POST['svx']['plugin'], $opt['std'], false ); } if ( isset( $opt['solids'] ) && !empty( $opt['solids'] ) && is_array( $opt['solids'] ) ) { foreach( $opt['solids'] as $key => $solid ) { update_option( $key, $solid, false ); } } wp_send_json( array( 'success' => true ) ); exit; } wp_send_json( array( 'success' => false ) ); exit; break; case 'backup' : $bkp = array(); $stg = isset( $_POST['svx']['settings'] ) && is_array( $_POST['svx']['settings'] ) ? $_POST['svx']['settings'] : array(); if ( isset( $stg['auto'] ) && !empty( $stg['auto'] ) && is_array( $stg['auto'] ) ) { $backup_auto = get_option( 'svx_autoload', array() ); foreach( $stg['auto'] as $k ) { if ( isset( $backup_auto[$k] ) ) { $bkp['auto'][$k] = $backup_auto[$k]; } } } if ( isset( $stg['std'] ) && !empty( $stg['std'] ) && is_array( $stg['std'] ) ) { $bkp['std'] = get_option( 'svx_settings_' . $_POST['svx']['plugin'], array() ); } if ( isset( $stg['solids'] ) && !empty( $stg['solids'] ) && is_array( $stg['solids'] ) ) { foreach( $stg['solids'] as $k ) { $bkp['solids'][$k] = get_option( $k ); } } $bkp['time'] = time(); update_option( '_svx_settings_backup_' . $_POST['svx']['plugin'], $bkp ); wp_send_json( array( 'success' => true ) ); exit; break; case 'restore' : $bkp = get_option( '_svx_settings_backup_' . $_POST['svx']['plugin'] ); if ( isset( $bkp['auto'] ) && !empty( $bkp['auto'] ) && is_array( $bkp['auto'] ) ) { $bkp['auto'] = array_merge( get_option( 'svx_autoload', array() ), $bkp['auto'] ); update_option( 'svx_autoload', $bkp['auto'], true ); } if ( isset( $bkp['std'] ) && !empty( $bkp['std'] ) && is_array( $bkp['std'] ) ) { update_option( 'svx_settings_' . $_POST['svx']['plugin'], $bkp['std'], false ); } if ( isset( $bkp['solids'] ) && !empty( $bkp['solids'] ) && is_array( $bkp['solids'] ) ) { foreach( $bkp['solids'] as $key => $solid ) { update_option( $key, $solid, false ); } } wp_send_json( array( 'success' => true ) ); exit; break; case 'reset' : $stg = isset( $_POST['svx']['settings'] ) && is_array( $_POST['svx']['settings'] ) ? $_POST['svx']['settings'] : array(); if ( isset( $stg['auto'] ) && !empty( $stg['auto'] ) && is_array( $stg['auto'] ) ) { $opt = get_option( 'svx_autoload', array() ); foreach( $opt as $k => $v ) { if ( in_array( $k, $stg['auto'] ) ) { unset( $opt[$k] ); } } update_option( 'svx_autoload', $opt, true ); } delete_option( 'svx_settings_' . $_POST['svx']['plugin'] ); if ( $_POST['svx']['plugin'] == 'product_filter' ) { if ( get_option( 'wc_settings_prdctfltr_version', false ) !== false ) { delete_option( 'wc_settings_prdctfltr_version' ); } } if ( !empty( $stg['solids'] ) && is_array( $stg['solids'] ) ) { foreach( $stg['solids'] as $key ) { delete_option( $key ); } } wp_send_json( array( 'success' => true ) ); exit; break; case 'register' : SevenVX()->_check_register(); break; case 'license_details' : SevenVX()->_check_license(); break; case 'filter' : SevenVX()->_filter_get(); break; default : SevenVX()->ajax_die($opt); break; } }
That function lacks a capability check to prevent unauthorized users to access it, and a security nonce to protect against cross-site request forgery attacks.
Any logged-in users or WooCommerce customers can access that function and perform the following actions:
- Line 965: Modify any WordPress options in the DB. For instance, redirect all traffic to an external malicious website by changing
siteurl
, create an administrator account by enabling registration (users_can_register
) and setting the default role (default_role
) to “administrator”, change the administrator email address (admin_email
) among many other possibilities. - Line 814: Delete any WordPress options in the DB.
- Line 766: Read any WordPress options from the DB.
- Lines 945, 918 and 808: Import, export and change the plugin settings.
- Line 769: Retrieve the list of all users on the blog.
Recommendations
Update immediately if you have any of the above vulnerable plugins. 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 developers were contacted on August 17, 2021 and again on August 31. The issue was eventually escalated to Envato on September 05 and all plugins were updated on September 07, 2021.
Stay informed about the latest vulnerabilities
- Running WordPress? You can get email notifications about vulnerabilities in the plugins or themes installed on your blog.
- On Twitter: @nintechnet