Headline
CVE-2023-4939: Helper.php in salesmanago/trunk/src/Includes – WordPress Plugin Repository
The SALESmanago plugin for WordPress is vulnerable to Log Injection in versions up to, and including, 3.2.4. This is due to the use of a weak authentication token for the /wp-json/salesmanago/v1/callbackApiV3 API endpoint which is simply a SHA1 hash of the site URL and client ID found in the page source of the website. This makes it possible for unauthenticated attackers to inject arbitrary content into the log files, and when combined with another vulnerability this could have significant consequences.
1<?php23namespace bhr\Includes;45use bhr\Admin\Entity\Configuration;6use Error;7use Exception;8use SALESmanago\Model\Collections\Api\V3\ProductsCollection;91011trait Helper {1213 /**14 * @param $input - array or string with commas15 * @param bool $returnString - true to receive string, false for array16 * @param false $removeInnerSpaces - spaces within items will be replaced with underscore17 * @param false $toUpperCase18 * @param string $separator - use separator different from comma19 * @param int $maxLength - maximum length per value20 *21 * @return array|string22 */23 public static function clearCSVInput(24 $input,25 $returnString = true,26 $removeInnerSpaces = false,27 $toUpperCase = false,28 $separator = ',’,29 $maxLength = 25530 ) {31 if ( empty( $input ) ) {32 return $returnString ? ‘’ : array();33 }34 if ( is_string( $input ) && ! is_array( $input ) ) {35 $inputArray = explode( ',’, $input );36 } else {37 $inputArray = $input;38 }39 $output = array();40 foreach ( $inputArray as $item ) {41 if ( trim( $item ) ) {42 $outItem = $toUpperCase43 ? strtoupper( trim( $item ) )44 : trim( $item );45 $outItem = $removeInnerSpaces46 ? str_replace( ' ', '_’, $outItem )47 : $outItem;48 $output[] = substr( $outItem, 0, $maxLength );49 }50 }5152 return $returnString53 ? implode( $separator, $output )54 : $output;55 }5657 /**58 * @return string59 */60 public static function getUserLocale() {61 if ( function_exists( ‘get_user_locale’ ) ) {62 return get_user_locale();63 }6465 return '’;66 }6768 /**69 * @return string70 */71 public static function getLocation() {72 try {73 $shopLocation = null;74 if ( function_exists( ‘wc_get_page_id’ ) ) {75 $shopLocation = ( get_permalink( wc_get_page_id( ‘shop’ ) ) == - 1 )76 ? get_home_url()77 : get_permalink( wc_get_page_id( ‘shop’ ) );78 }79 if ( empty( $shopLocation ) ) {80 $shopLocation = $_SERVER[‘SERVER_NAME’];81 }8283 return GlobalConstant::LOCATION_PREFIX . md5( $shopLocation );84 } catch ( Error $e ) {85 error_log( $e->getMessage() );8687 return GlobalConstant::LOCATION_PREFIX . md5( strval( rand( 1, 100 ) ) );88 }89 }9091 /**92 * @return string93 */94 public static function getLanguage( $lang ) {95 return $lang === 'browser’96 ? substr( $_SERVER[‘HTTP_ACCEPT_LANGUAGE’], 0, 2 )97 : substr( Helper::getUserLocale(), 0, 2 );98 }99100 /**101 * @param $param102 *103 * @return false|\WC_Product|null104 */105 public static function wcGetProduct( $param ) {106 return wc_get_product( $param );107 }108109 /**110 * @param $hook_name111 * @param $callback112 * @param int $priority113 * @param int $accepted_args114 */115 public static function addAction( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {116 add_action( $hook_name, $callback, $priority, $accepted_args );117 }118119 /**120 * @param $plugin121 * @param $deprecated122 * @param $path123 */124 public static function loadPluginTextDomain( $plugin, $deprecated, $path ) {125 if ( function_exists( ‘load_plugin_textdomain’ ) ) {126 load_plugin_textdomain( $plugin, $deprecated, $path );127 }128 }129130 /**131 * @param $param132 *133 * @return bool|\WC_Order|\WC_Order_Refund134 */135 public static function wcGetOrder( $param ) {136 return wc_get_order( $param );137 }138139 /**140 * @return \WooCommerce141 */142 public static function wc() {143 return WC();144 }145146 /**147 * @param $orderId148 * @param $productIdentifierType149 *150 * @return array151 */152 public static function getProductsFromOrder( $orderId, $productIdentifierType ) {153 /* Products */154 $order = self::wcGetOrder( $orderId );155 $ids = array();156 $variantIds = array();157 $names = array();158 $quantities = array();159 $skus = array();160 $smProductArray = array();161162 foreach ( $order->get_items() as $item_id => $item ) {163 $WcProduct = Helper::wcGetProduct( $item[‘variation_id’] )164 ? Helper::wcGetProduct( $item[‘variation_id’] )165 : Helper::wcGetProduct( $item[‘product_id’] );166 $smProductArray[] = self::getSmEventDetailsFromWcProduct( $WcProduct );167 $quantities[] = $item->get_quantity();168 }169170 foreach ( $smProductArray as $SmProduct ) {171 $ids[] = $SmProduct->getId();172 $variantIds[] = $SmProduct->getVariantId();173 $skus[] = $SmProduct->getSku();174 $names[] = $SmProduct->getName();175 }176177 /* Shipping */178 $shippingMethodNames = array();179180 foreach ( $order->get_items( ‘shipping’ ) as $item_id => $item ) {181 $shippingMethodNames[] = $item->get_method_title();182 }183 $shippingMethodName = implode( ',’, $shippingMethodNames );184185 $products = array(186 ‘description’ => $order->get_payment_method(),187 ‘value’ => $order->get_total(),188 ‘detail1’ => implode( ',’, $names ),189 ‘detail2’ => $order->get_order_key(),190 ‘detail3’ => implode( '/’, $quantities ),191 ‘detail4’ => $order->get_customer_note(),192 ‘detail5’ => $shippingMethodName,193 ‘externalId’ => $order->get_id(),194 );195196 $products += self::generateProductsDetailsByIdentifierType(197 $productIdentifierType,198 $ids,199 $variantIds,200 $skus201 );202203 return $products;204 }205206 /**207 * @param $productIdentifierType208 * @param $ids209 * @param $skus210 * @param $variantIds211 *212 * @return array213 */214 public static function generateProductsDetailsByIdentifierType(215 $productIdentifierType = '’,216 $ids = array(),217 $variantIds = array(),218 $skus = array()219 ) {220 switch ( $productIdentifierType ) {221 case 'sku’:222 $product[‘products’] = implode( ',’, $skus );223 $product[‘detail6’] = implode( ',’, $ids );224 $product[‘detail7’] = implode( ',’, $variantIds );225 break;226227 case 'variant Id’:228 $product[‘products’] = implode( ',’, $variantIds );229 $product[‘detail6’] = implode( ',’, $ids );230 $product[‘detail7’] = implode( ',’, $skus );231 break;232233 default:234 $product[‘products’] = implode( ',’, $ids );235 $product[‘detail6’] = implode( ',’, $skus );236 $product[‘detail7’] = implode( ',’, $variantIds );237 break;238 }239240 return $product;241 }242243 /**244 * @param $WcProduct245 *246 * @return SmProduct247 */248 public static function getSmEventDetailsFromWcProduct( $WcProduct ) {249 $SmProduct = new SmProduct();250251 /* Simple products have no parent */252 $WcProduct->get_parent_id() !== 0253 ? $SmProduct->setId( $WcProduct->get_parent_id() )254 : $SmProduct->setId( $WcProduct->get_id() );255256 $SmProduct->setVariantId( $WcProduct->get_id() )257 ->setSku( $WcProduct->get_sku() )258 ->setUnitPrice( $WcProduct->get_price() )259 ->setName( $WcProduct->get_name() );260261 return $SmProduct;262 }263264 /**265 * @param $postId266 * @param $key267 * @param $single268 *269 * @return mixed270 */271 public static function getPostMetaData( $postId, $key, $single ) {272 return get_post_meta( $postId, $key, $single );273 }274275 /**276 * @param $type277 * @param $arg278 *279 * @return false|\WP_User280 */281 public static function getUserBy( $type, $arg ) {282 return get_user_by( $type, $arg );283 }284285 /**286 * Verify if endpoint starts with https287 *288 * @param string $endpoint289 *290 * @return false|int291 */292 public static function checkEndpointForHTTPS( $endpoint ) {293 return preg_match( '/^(https:\/\/)+/’, $endpoint );294 }295296 /**297 * Make sure that salesmanago plugin is loaded last so that woocommerce functions can be used298 *299 * @return void300 */301 public static function loadSMPluginLast() {302 $SMPlugin = "salesmanago/salesmanago.php";303 $activePlugins = get_option( ‘active_plugins’ );304 $thisPluginKey = array_search( $SMPlugin, $activePlugins );305306 if ( in_array( $SMPlugin, $activePlugins ) && end( $activePlugins ) !== $SMPlugin ) {307 array_splice( $activePlugins, $thisPluginKey, 1 );308 array_push( $activePlugins, $SMPlugin );309 update_option( 'active_plugins’, $activePlugins );310 }311 }312313 /**314 * Helper function to get image url315 *316 * @param $image_tag string317 *318 * @return string319 */320 public static function getImageUrl( $image_tag ) {321 $str = explode( 'src=’, $image_tag )[1];322323 return trim( explode( ' ', $str )[0], ‘"’ );324 }325326 /**327 * Generate webhook callback url for Product API328 *329 * @return string330 */331 public static function generate_api_v3_webhook_url() {332 $url = get_site_url();333334 return $url . GlobalConstant::API_V3_CALLBACK_URL . ‘?sm_token=’ . self::generate_sm_token();335 }336337 /**338 * Extract WP product id from error message array.339 * Iterate over array of error messages, extracting the index of the product that caused the exception.340 * The index is then used to extract WP product ID from the $products array.341 * Max 8 errors are displayed in the console at a time.342 *343 * @param array $message_array Array of string error message from ApiException344 * @param ProductsCollection $products Collection of products345 *346 * @return string|false Readable error message or false on failure347 */348 public static function extract_product_id_from_error_message_array( $message_array, $products ) {349 $preg_pattern = '/\[(\d*?)\]/’;350 $errors_to_be_displayed = [];351 $max_err_displayed = 8;352 try {353 // additional condition $i < $max_err_displayed because we don’t want to overwhelm the user with errors354 $num_of_messages = count( $message_array );355 for ( $i = 0; $i < $num_of_messages && $i < $max_err_displayed; $i ++ ) {356 $matches = array();357 preg_match( $preg_pattern, $message_array[ $i ], $matches );358 $arr_index = $matches[1]; // first captured parenthesized subpattern holds the index359 $productId = $products->toArray()[ intval( $arr_index ) ][‘productId’];360 $errors_to_be_displayed[] = "{$message_array[ $i ]}. Product ID:{$productId}\n";361 }362 sort( $errors_to_be_displayed );363364 return implode( $errors_to_be_displayed );365 } catch ( Error | Exception $ex ) {366 return false;367 }368 }369370371 /**372 * Generate token for api v3 callback verification373 *374 * @return string token375 */376 public static function generate_sm_token() {377 $websiteUrl = get_site_url();378 $clientId = Configuration::getInstance()->getClientId();379 $apiKey = Configuration::getInstance()->getApiKey();380 return hash('sha256’, substr(sha1($websiteUrl . $clientId), 0, 16) . substr($apiKey, (strlen($apiKey) - 8) / 2, 8));381 }382}