CVE-2023-45958: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Thirty Bees Core v1.4.0 was discovered to contain a reflected cross-site scripting (XSS) vulnerability via the backup_pagination parameter at /controller/AdminController.php. This vulnerability allows attackers to execute arbitrary JavaScript in the web browser of a user via a crafted payload.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-10-18T22:15Z
Repository: https://github.com/thirtybees/thirtybees
Language: PHP
Files changed: classes/controller/AdminController.php, classes/helper/HelperList.php
Commit date: 2023-10-08 10:32:05 +0200
Commit message: Fix XSS vulnerability related to list pagination

Special thanks to Ulaş Deniz İlhan from Zigrin Security for discovering
and reporting this vulnerability.

Diff: diff --git a/classes/controller/AdminController.php b/classes/controller/AdminController.php
index e751f5af3e..e112f70fff 100644
--- a/classes/controller/AdminController.php
+++ b/classes/controller/AdminController.php
@@ -955,34 +955,30 @@ class AdminControllerCore extends Controller
         $this->dispatchFieldsListingModifierEvent();
 
         $this->ensureListIdDefinition();
 
         /* Manage default params values */
-        $useLimit = true;
         if ($limit === false) {
             $useLimit = false;
-        } elseif (empty($limit)) {
-            if (isset($this->context->cookie->{$this->list_id.'_pagination'}) && $this->context->cookie->{$this->list_id.'_pagination'}) {
-                $limit = (int)$this->context->cookie->{$this->list_id.'_pagination'};
+        } else {
+            $useLimit = true;
+            $limit = HelperList::resolvePagination($this->list_id, $this->context->cookie, $this->_pagination, $this->_default_pagination);
+            if ($limit !== $this->_default_pagination) {
+                $this->context->cookie->{$this->list_id.'_pagination'} = $limit;
             } else {
-                $limit = (int)$this->_default_pagination;
+                unset($this->context->cookie->{$this->list_id.'_pagination'});
             }
         }
 
         if (!Validate::isTableOrIdentifier($this->table)) {
             throw new PrestaShopException(sprintf('Table name %s is invalid:', $this->table));
         }
 
         $orderBy = $this->resolveOrderBy($orderBy);
         $orderWay = $this->resolveOrderWay($orderWay);
 
-        $limit = Tools::getIntValue($this->list_id.'_pagination', $limit);
-        if (in_array($limit, $this->_pagination) && $limit != $this->_default_pagination) {
-            $this->context->cookie->{$this->list_id.'_pagination'} = $limit;
-        } else {
-            unset($this->context->cookie->{$this->list_id.'_pagination'});
-        }
+
 
         /* Check params validity */
         if (!Validate::isOrderBy($orderBy) || !Validate::isOrderWay($orderWay)) {
             throw new PrestaShopException(sprintf(Tools::displayError('Invalid ordering parameters: orderBy=[%s] orderWay=[%s]'), $orderBy, $orderWay));
         }
diff --git a/classes/helper/HelperList.php b/classes/helper/HelperList.php
index 007847743c..85c1fc4a1a 100644
--- a/classes/helper/HelperList.php
+++ b/classes/helper/HelperList.php
@@ -307,46 +307,26 @@ class HelperListCore extends Helper
         if (empty($token)) {
             $token = $this->token;
         }
 
         /* Determine total page number */
-        $pagination = $this->_default_pagination;
-        $cookie = $this->context->cookie;
-        if (in_array(Tools::getIntValue($this->list_id.'_pagination'), $this->_pagination)) {
-            $pagination = Tools::getIntValue($this->list_id.'_pagination');
-        } elseif (isset($cookie->{$this->list_id.'_pagination'}) && $cookie->{$this->list_id.'_pagination'}) {
-            $pagination = $cookie->{$this->list_id.'_pagination'};
-        }
-
+        $pagination = $this->getSelectedPagination();
         $totalPages = max(1, ceil($this->listTotal / $pagination));
 
         $identifier = Tools::getIsset($this->identifier) ? '&'.$this->identifier.'='.Tools::getIntValue($this->identifier) : '';
-//        $order = '';
-//        if (Tools::getIsset($this->table.'Orderby')) {
-//            $order = '&'.$this->table.'Orderby='.urlencode($this->orderBy).'&'.$this->table.'Orderway='.urlencode(strtolower($this->orderWay));
-//        }
 
         $action = $this->currentIndex.$identifier.'&token='.$token.'#'.$this->list_id;
 
         /* Determine current page number */
         $page = Tools::getIntValue('submitFilter'.$this->list_id);
-
-        if (!$page) {
+        if ($page <= 0) {
             $page = 1;
         }
-
         if ($page > $totalPages) {
             $page = $totalPages;
         }
-
-        $this->page = (int) $page;
-
-        /* Choose number of results per page */
-        $selectedPagination = Tools::getValue(
-            $this->list_id.'_pagination',
-            $cookie->{$this->list_id . '_pagination'} ?? $this->_default_pagination
-        );
+        $this->page = (int)$page;
 
         if (is_null($this->table_id) && $this->position_identifier && Tools::getIntValue($this->position_identifier, 1)) {
             $this->table_id = substr($this->identifier, 3, strlen($this->identifier));
         }
 
@@ -355,10 +335,11 @@ class HelperListCore extends Helper
         }
 
         $prefix = str_replace(['admin', 'controller'], '', mb_strtolower((string)$this->controller_name));
         $ajax = false;
         $controller = $this->getController();
+        $cookie = $this->context->cookie;
         foreach ($this->fields_list as $key => $params) {
             if (!isset($params['type'])) {
                 $params['type'] = 'text';
             }
 
@@ -439,11 +420,11 @@ class HelperListCore extends Helper
         $this->context->smarty->assign(
             [
                 'page'                => $page,
                 'simple_header'       => $this->simple_header,
                 'total_pages'         => $totalPages,
-                'selected_pagination' => $selectedPagination,
+                'selected_pagination' => $this->getSelectedPagination(),
                 'pagination'          => $this->_pagination,
                 'list_total'          => $this->listTotal,
                 'sql'                 => str_replace('\n', ' ', str_replace('\r', '', (string)$this->sql)),
                 'table'               => $this->table,
                 'bulk_actions'        => $this->bulk_actions,
@@ -1020,6 +1001,62 @@ class HelperListCore extends Helper
             ]
         );
 
         return $tpl->fetch();
     }
+
+    /**
+     * @return int
+     */
+    protected function getSelectedPagination()
+    {
+        return static::resolvePagination($this->list_id, $this->context->cookie, $this->_pagination, $this->_default_pagination);
+    }
+
+    /**
+     * @param string $listId
+     * @param Cookie $cookie
+     * @param array $pagination
+     * @param int $defaultPagination
+     *
+     * @return int
+     */
+    public static function resolvePagination(string $listId, Cookie $cookie, array $pagination, int $defaultPagination)
+    {
+        if ($pagination) {
+            $value = static::resolvePaginationValue($listId, $cookie, $defaultPagination);
+            if (in_array($value, $pagination)) {
+                return $value;
+            }
+            if (in_array($defaultPagination, $pagination)) {
+                return $defaultPagination;
+            }
+            return $pagination[0];
+        } else {
+            trigger_error("Pagination not set for list $listId", E_USER_WARNING);
+            return 20;
+        }
+    }
+
+    /**
+     * @param string $listId
+     * @param Cookie $cookie
+     * @param int $defaultPagination
+     *
+     * @return int
+     */
+    protected static function resolvePaginationValue(string $listId, Cookie $cookie, int $defaultPagination)
+    {
+        $paginationKey = $listId.'_pagination';
+        $pagination = Tools::getIntValue($paginationKey);
+        if ($pagination > 0) {
+            return $pagination;
+        }
+        if (isset($cookie->{$paginationKey})) {
+            $pagination = (int)$cookie->{$paginationKey};
+            if ($pagination > 0) {
+                return $pagination;
+            }
+        }
+        return $defaultPagination;
+    }
 }

Vulnerable code:         $useLimit = true;
        } elseif (empty($limit)) {
            if (isset($this->context->cookie->{$this->list_id.'_pagination'}) && $this->context->cookie->{$this->list_id.'_pagination'}) {
                $limit = (int)$this->context->cookie->{$this->list_id.'_pagination'};
                $limit = (int)$this->_default_pagination;
        $limit = Tools::getIntValue($this->list_id.'_pagination', $limit);
        if (in_array($limit, $this->_pagination) && $limit != $this->_default_pagination) {
            $this->context->cookie->{$this->list_id.'_pagination'} = $limit;
        } else {
            unset($this->context->cookie->{$this->list_id.'_pagination'});
        }
        $pagination = $this->_default_pagination;
        $cookie = $this->context->cookie;
        if (in_array(Tools::getIntValue($this->list_id.'_pagination'), $this->_pagination)) {
            $pagination = Tools::getIntValue($this->list_id.'_pagination');
        } elseif (isset($cookie->{$this->list_id.'_pagination'}) && $cookie->{$this->list_id.'_pagination'}) {
            $pagination = $cookie->{$this->list_id.'_pagination'};
        }

//        $order = '';
//        if (Tools::getIsset($this->table.'Orderby')) {
//            $order = '&'.$this->table.'Orderby='.urlencode($this->orderBy).'&'.$this->table.'Orderway='.urlencode(strtolower($this->orderWay));
//        }

        if (!$page) {


        $this->page = (int) $page;

        /* Choose number of results per page */
        $selectedPagination = Tools::getValue(
            $this->list_id.'_pagination',
            $cookie->{$this->list_id . '_pagination'} ?? $this->_default_pagination
        );
                'selected_pagination' => $selectedPagination,
Fixed code:         } else {
            $useLimit = true;
            $limit = HelperList::resolvePagination($this->list_id, $this->context->cookie, $this->_pagination, $this->_default_pagination);
            if ($limit !== $this->_default_pagination) {
                $this->context->cookie->{$this->list_id.'_pagination'} = $limit;
                unset($this->context->cookie->{$this->list_id.'_pagination'});

        $pagination = $this->getSelectedPagination();
        if ($page <= 0) {
        $this->page = (int)$page;
        $cookie = $this->context->cookie;
                'selected_pagination' => $this->getSelectedPagination(),

    /**
     * @return int
     */
    protected function getSelectedPagination()
    {
        return static::resolvePagination($this->list_id, $this->context->cookie, $this->_pagination, $this->_default_pagination);
    }

    /**
     * @param string $listId
     * @param Cookie $cookie
     * @param array $pagination
     * @param int $defaultPagination
     *
     * @return int
     */
    public static function resolvePagination(string $listId, Cookie $cookie, array $pagination, int $defaultPagination)
    {
        if ($pagination) {
            $value = static::resolvePaginationValue($listId, $cookie, $defaultPagination);
            if (in_array($value, $pagination)) {
                return $value;
            }
            if (in_array($defaultPagination, $pagination)) {
                return $defaultPagination;
            }
            return $pagination[0];
        } else {
            trigger_error("Pagination not set for list $listId", E_USER_WARNING);
            return 20;
        }
    }

    /**
     * @param string $listId
     * @param Cookie $cookie
     * @param int $defaultPagination
     *
     * @return int
     */
    protected static function resolvePaginationValue(string $listId, Cookie $cookie, int $defaultPagination)
    {
        $paginationKey = $listId.'_pagination';
        $pagination = Tools::getIntValue($paginationKey);
        if ($pagination > 0) {
            return $pagination;
        }
        if (isset($cookie->{$paginationKey})) {
            $pagination = (int)$cookie->{$paginationKey};
            if ($pagination > 0) {
                return $pagination;
            }
        }
        return $defaultPagination;
    }