[CVE-2023-47309] Improper Neutralization of Input During Web Page Generation in Nukium – NKM GLS module for PrestaShop

PrestaShop Security Advisory

In the module “NKM GLS” (nkmgls) up to version 3.0.1 from Nukium for PrestaShop, a guest (authenticated customer) can perform XSS injection of type 2 (stored XSS) from FRONT to BACK (F2B) of Category 2 within the funnel order in affected versions.

Note : To succeed in this exploit, the red team needs to pay to convert a cart into a valid order with GLS carrier and require the administrator of the PS to check a specific screen within its backoffice.

Summary

  • CVE ID: CVE-2023-47309
  • Published at: 2023-11-14
  • Platform: PrestaShop
  • Product: nkmgls
  • Impacted release: <= 3.0.1 (3.0.2 fixed the vulnerability)
  • Product author: Nukium
  • Weakness: CWE-79
  • Severity: critical (9.0)

Description

As all XSS type 2 (Stored XSS) F2B (Front to Back), there are two steps and a prerequisite.

Prerequisite :

  • The field phone_mobile within table gls_cart_carrier suffers from a type varchar(255) which is large enough to allow dangerous XSS payloads.

Steps :

  • The method NkmGlsCheckoutModuleFrontController::displayAjaxSavePhoneMobile does not properly clean the parameter gls_customer_mobile. pSQL is useless against XSS which exploits HTML tag attributes (Category 2 according to OWASP – pSQL only neutralized Category 1 thanks to its strip_tags).
  • The output in the backoffice is not escaped in the related smarty template that uses it.

CVSS base metrics

  • Attack vector: network
  • Attack complexity: low
  • Privilege required: low
  • User interaction: required
  • Scope: changed
  • Confidentiality: high
  • Integrity: high
  • Availability: high

Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

Possible malicious usage

  • Unlock design critical vulnerabilities, see this.

Patch from 3.0.2

--- a/controllers/front/checkout.php
+++ b/controllers/front/checkout.php

 +use NukiumGLSCommonExceptionGlsException;
 use NukiumGLSCommonLegacyGlsController;
 
 require_once dirname(__FILE__) . '/../../vendor/autoload.php';
@@ -25,54 +26,63 @@ class NkmGlsCheckoutModuleFrontController extends ModuleFrontController
             'message' => ''
         );
 
-        $phone_mobile = Tools::getValue('gls_customer_mobile');
-        $id_carrier = Tools::getValue('id_carrier');
-        $is_relay = Tools::getValue('is_relay');
-
-        if ($cart && !empty($id_carrier)) {
-            /**
-             * Récupération du code produit GLS (dépend du transporteur sélectionné uniquement dans ce cas de figure
-             * car l'enregistrement est appelé uniquement pour les transporteurs gls13h ou glsrelais)
-             */
-            $customer_address = new Address($cart->id_address_delivery);
-            $customer_country_iso = '';
-            if ($customer_address) {
-                $customer_country_iso = Country::getIsoById($customer_address->id_country);
+        try {
+            $phone_mobile = Tools::getValue('gls_customer_mobile');
+            if (!Validate::isPhoneNumber($phone_mobile)) {
+                throw new GlsException($this->module->l('Please fill-in a valid mobile number (e.g. +XXXXXXXXXXX or 0XXXXXXXXX).', 'nkmgls'));
             }
-            $gls_product = $this->module->getGlsProductCode(
-                (int)$id_carrier,
-                $customer_country_iso
-            );
 
-            $query = new DbQuery();
-            $query->select('c.*')
-                ->from('gls_cart_carrier', 'c')
-                ->where('c.`id_customer` = ' . (int) $cart->id_customer)
-                ->where('c.`id_cart` = ' . (int) $cart->id);
-
-            if (Db::getInstance()->getRow($query)) {
-                $sql = 'UPDATE ' . _DB_PREFIX_ . 'gls_cart_carrier SET `customer_phone_mobile`='' . pSQL($phone_mobile) . '', `id_carrier`=' . (int) $id_carrier . ', `gls_product`='' . pSQL($gls_product) . ''';
-                // reset all data except mobile and gls_product
-                if (! $is_relay) {
-                    $sql .= ',`parcel_shop_id` = NULL, `name` = NULL, `address1` = NULL, `address2` = NULL, `postcode` = NULL,
-                        `city` = NULL, `phone` = NULL, `phone_mobile` = NULL, `id_country` = NULL, `parcel_shop_working_day` = NULL';
+            $id_carrier = Tools::getValue('id_carrier');
+            $is_relay = Tools::getValue('is_relay');
+
+            if ($cart && !empty($id_carrier)) {
+                /**
+                 * Récupération du code produit GLS (dépend du transporteur sélectionné uniquement dans ce cas de figure
+                 * car l'enregistrement est appelé uniquement pour les transporteurs gls13h ou glsrelais)
+                 */
+                $customer_address = new Address($cart->id_address_delivery);
+                $customer_country_iso = '';
+                if ($customer_address) {
+                    $customer_country_iso = Country::getIsoById($customer_address->id_country);
                 }
-                $sql .= ' WHERE `id_customer`=' . (int) $cart->id_customer . ' AND `id_cart`=' . (int) $cart->id;
+                $gls_product = $this->module->getGlsProductCode(
+                    (int)$id_carrier,
+                    $customer_country_iso
+                );
+
+                $query = new DbQuery();
+                $query->select('c.*')
+                    ->from('gls_cart_carrier', 'c')
+                    ->where('c.`id_customer` = ' . (int) $cart->id_customer)
+                    ->where('c.`id_cart` = ' . (int) $cart->id);
+
+                if (Db::getInstance()->getRow($query)) {
+                    $sql = 'UPDATE ' . _DB_PREFIX_ . 'gls_cart_carrier SET `customer_phone_mobile`='' . pSQL($phone_mobile) . '', `id_carrier`=' . (int) $id_carrier . ', `gls_product`='' . pSQL($gls_product) . ''';
+                    // reset all data except mobile and gls_product
+                    if (! $is_relay) {
+                        $sql .= ',`parcel_shop_id` = NULL, `name` = NULL, `address1` = NULL, `address2` = NULL, `postcode` = NULL,
+                            `city` = NULL, `phone` = NULL, `phone_mobile` = NULL, `id_country` = NULL, `parcel_shop_working_day` = NULL';
+                    }
+                    $sql .= ' WHERE `id_customer`=' . (int) $cart->id_customer . ' AND `id_cart`=' . (int) $cart->id;
 
-                if (Db::getInstance()->Execute($sql)) {
-                    $return['result'] = true;
-                } else {
-                    $return['message'] = $this->module->l('Unexpected error occurred.', self::L_SPECIFIC);
-                }
-            } else {
-                if (Db::getInstance()->Execute('INSERT INTO ' . _DB_PREFIX_ . 'gls_cart_carrier
-                    (`id_customer`, `id_cart`, `id_carrier`, `gls_product`, `customer_phone_mobile`)
-                    VALUES (' . (int) $cart->id_customer . ', ' . (int) $cart->id . ', ' . (int) $id_carrier . ', '' . pSQL($gls_product) . '', '' . pSQL($phone_mobile) . '')')) {
-                    $return['result'] = true;
+                    if (Db::getInstance()->Execute($sql)) {
+                        $return['result'] = true;
+                    } else {
+                        throw new GlsException($this->module->l('Unexpected error occurred.', self::L_SPECIFIC));
+                    }
                 } else {
-                    $return['message'] = $this->module->l('Unexpected error occurred.', self::L_SPECIFIC);
+                    if (Db::getInstance()->Execute('INSERT INTO ' . _DB_PREFIX_ . 'gls_cart_carrier
+                        (`id_customer`, `id_cart`, `id_carrier`, `gls_product`, `customer_phone_mobile`)
+                        VALUES (' . (int) $cart->id_customer . ', ' . (int) $cart->id . ', ' . (int) $id_carrier . ', '' . pSQL($gls_product) . '', '' . pSQL($phone_mobile) . '')')) {
+                        $return['result'] = true;
+                    } else {
+                        throw new GlsException($this->module->l('Unexpected error occurred.', self::L_SPECIFIC));
+                    }
                 }
             }
+        } catch (GlsException $e) {
+            $return['result'] = false;
+            $return['message'] = $e->getMessage();
         }
 
         header('Content-Type: application/json');
--- a/views/templates/admin/gls_label/label_list.tpl
+++ b/views/templates/admin/gls_label/label_list.tpl

@@ -186,7 +186,7 @@
                                                         {l s='Mobile' mod='nkmgls'}
                                                     
                                                     
- +
{if $tr.id_country|in_array:$cee_countries === false} @@ -210,10 +210,10 @@

Other recommendations

  • It’s recommended to upgrade to the latest version of the module nkmgls.
  • Systematically escape characters ‘ “ < and > by replacing them with HTML entities and applying strip_tags – Smarty and Twig provide auto-escape filters :
    • Smarty: {$value.comment|escape:'html':'UTF-8'}
    • Twig:{{value.comment|e}}
  • Limit to the strict minimum the length’s value in database – a database field that allow 10 characters (varchar(10)) is far less dangerous than a field that allows 40+ characters (use cases that can exploit fragmented XSS payloads are very rare)
  • Configure CSP headers (content security policies) by listing external domains allowed to load assets (such as js files) or being called in XHR transactions (Ajax).
  • If applicable: check against all your frontoffice’s uploaders, uploading files that will be served by your server that mime type application/javascript (like every .js natively) must be strictly forbidden as it must be considered as dangerous as PHP files.
  • Activate OWASP 941’s rules on your WAF (Web application firewall) – be warned that you will probably break your frontoffice/backoffice and you will need to preconfigure some bypasses against this set of rules.

Timeline

DateAction
2023-02-24Issue discovered during a code review by TouchWeb.fr
2023-02-24Contact the author who will provide a patch within 4 hours
2023-02-24V3.0.2 available on https://store.nukium.com/ and https://addons.prestashop.com/
2023-03-10Recontact author about the publication of the vulnerability
2023-03-12Author completes the diff of this present CVE and asks for a delay to publish
2023-05-05Recontact author about the publication of the vulnerability
2023-05-17Author ask for another delay before publication
2023-07-15Recontact author about the publication of the vulnerability
2023-09-17Recontact author about the publication of the vulnerability
2023-09-30Recontact author about the publication of the vulnerability
2023-10-30Inform the author about the publication of the vulnerability
2023-10-30Request a CVE ID
2023-11-08Received CVE ID
2023-11-14Publish this security advisory

READ MORE