CVE-2021-41261: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Galette is a membership management web application built for non profit organizations and released under GPLv3. Versions prior to 0.9.6 are subject to stored cross site scripting attacks via the preferences footer. The preference footer can only be altered by a site admin. This issue has been resolved in the 0.9.6 release and all users are advised to upgrade. There are no known workarounds.
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: 4.8
Published: 2021-12-16T19:15Z
Repository: https://github.com/galette/galette
Language: PHP
Files changed: galette/composer.json, galette/composer.lock, galette/lib/Galette/Controllers/Crud/PaymentTypeController.php, galette/lib/Galette/Core/Preferences.php, galette/lib/Galette/Entity/Entitled.php, galette/lib/Galette/Entity/Title.php, galette/lib/Galette/Entity/Transaction.php, galette/templates/default/gestion_intitule_content.tpl, galette/templates/default/gestion_titres.tpl, galette/templates/default/gestion_transactions.tpl
Commit date: 2021-11-09 22:51:39 +0100
Commit message: Fix XSS, prevent their storage

Use HTML purifier lib to sanitize preferences footer

Diff: diff --git a/galette/composer.json b/galette/composer.json
index 7188763d5..f33d8d005 100644
--- a/galette/composer.json
+++ b/galette/composer.json
@@ -47,11 +47,12 @@
         "psr/http-message": "^1.0",
         "psr/container": "^1.0",
         "php-di/slim-bridge": "2.0.0",
         "doctrine/annotations": "^1.8",
         "laminas/laminas-servicemanager": "3.7",
-        "symfony/polyfill-php80": "^1.23"
+        "symfony/polyfill-php80": "^1.23",
+        "ezyang/htmlpurifier": "^4.13"
     },
     "require-dev": {
         "atoum/atoum": "dev-master",
         "galette/smarty-xgettext": "^1.2",
         "squizlabs/php_codesniffer": "^3.5"
diff --git a/galette/composer.lock b/galette/composer.lock
index 41ae74895..79e9819a4 100644
--- a/galette/composer.lock
+++ b/galette/composer.lock
@@ -2,11 +2,11 @@
     "_readme": [
         "This file locks the dependencies of your project to a known state",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "abdb30cee5eeb0dad05c159a1c10880d",
+    "content-hash": "47d56d47701d9038105bd93cf3b44a02",
     "packages": [
         {
             "name": "akrabat/rka-slim-session-middleware",
             "version": "2.0.0",
             "source": {
@@ -364,10 +364,64 @@
                     "type": "tidelift"
                 }
             ],
             "time": "2020-05-25T17:44:05+00:00"
         },
+        {
+            "name": "ezyang/htmlpurifier",
+            "version": "v4.13.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ezyang/htmlpurifier.git",
+                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2"
+            },
+            "require-dev": {
+                "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "HTMLPurifier": "library/"
+                },
+                "files": [
+                    "library/HTMLPurifier.composer.php"
+                ],
+                "exclude-from-classmap": [
+                    "/library/HTMLPurifier/Language/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Edward Z. Yang",
+                    "email": "admin@htmlpurifier.org",
+                    "homepage": "http://ezyang.com"
+                }
+            ],
+            "description": "Standards compliant HTML filter written in PHP",
+            "homepage": "http://htmlpurifier.org/",
+            "keywords": [
+                "html"
+            ],
+            "support": {
+                "issues": "https://github.com/ezyang/htmlpurifier/issues",
+                "source": "https://github.com/ezyang/htmlpurifier/tree/master"
+            },
+            "time": "2020-06-29T00:56:53+00:00"
+        },
         {
             "name": "laminas/laminas-cache",
             "version": "2.13.0",
             "source": {
                 "type": "git",
diff --git a/galette/lib/Galette/Controllers/Crud/PaymentTypeController.php b/galette/lib/Galette/Controllers/Crud/PaymentTypeController.php
index f5072d48a..af1796e71 100644
--- a/galette/lib/Galette/Controllers/Crud/PaymentTypeController.php
+++ b/galette/lib/Galette/Controllers/Crud/PaymentTypeController.php
@@ -5,11 +5,11 @@
 /**
  * Galette payment types controller
  *
  * PHP version 5
  *
- * Copyright © 2019-2020 The Galette Team
+ * Copyright © 2019-2021 The Galette Team
  *
  * This file is part of Galette (http://galette.tuxfamily.org).
  *
  * Galette is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -26,11 +26,11 @@
  *
  * @category  Controllers
  * @package   Galette
  *
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2019-2020 The Galette Team
+ * @copyright 2019-2021 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2019-12-09
  */
 
@@ -48,11 +48,11 @@ use Analog\Analog;
  *
  * @category  Controllers
  * @name      PaymentTypeController
  * @package   Galette
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2019-2020 The Galette Team
+ * @copyright 2019-2021 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.9.4dev - 2019-12-09
  */
 
@@ -203,20 +203,20 @@ class PaymentTypeController extends CrudController
             if ($id === null) {
                 $this->flash->addMessage(
                     'error_detected',
                     preg_replace(
                         '(%s)',
-                        $ptype->name,
+                        $ptype->getName(),
                         _T("Payment type '%s' has not been added!")
                     )
                 );
             } else {
                 $this->flash->addMessage(
                     'error_detected',
                     preg_replace(
                         '(%s)',
-                        $ptype->name,
+                        $ptype->getName(),
                         _T("Payment type '%s' has not been modified!")
                     )
                 );
                 //redirect to payment type edition
                 $redirect_uri = $this->router->pathFor('editPaymentType', ['id' => $id]);
@@ -225,20 +225,20 @@ class PaymentTypeController extends CrudController
             if ($id === null) {
                 $this->flash->addMessage(
                     'success_detected',
                     preg_replace(
                         '(%s)',
-                        $ptype->name,
+                        $ptype->getName(),
                         _T("Payment type '%s' has been successfully added.")
                     )
                 );
             } else {
                 $this->flash->addMessage(
                     'success_detected',
                     preg_replace(
                         '(%s)',
-                        $ptype->name,
+                        $ptype->getName(),
                         _T("Payment type '%s' has been successfully modified.")
                     )
                 );
             }
         }
diff --git a/galette/lib/Galette/Core/Preferences.php b/galette/lib/Galette/Core/Preferences.php
index 66395fa14..e488c4c8c 100644
--- a/galette/lib/Galette/Core/Preferences.php
+++ b/galette/lib/Galette/Core/Preferences.php
@@ -753,10 +753,13 @@ class Preferences
             case 'pref_card_year':
                 if ($value !== 'DEADLINE' && !preg_match('/^(?:\d{4}|\d{2})(\D?)(?:\d{4}|\d{2})$/', $value)) {
                     $this->errors[] = _T("- Invalid year for cards.");
                 }
                 break;
+            case 'pref_footer':
+                $value = $this->cleanHtmlValue($value);
+                break;
         }
 
         return $value;
     }
 
@@ -959,10 +962,12 @@ class Preferences
             if (
                 GALETTE_MODE === Galette::MODE_DEMO
                 && $name == 'pref_mail_method'
             ) {
                 return GaletteMail::METHOD_DISABLED;
+            } elseif ($name == 'pref_footer') {
+                return $this->cleanHtmlValue($this->prefs[$name]);
             } else {
                 if ($name == 'pref_adhesion_form' && $this->prefs[$name] == '') {
                     $this->prefs[$name] = self::$defaults['pref_adhesion_form'];
                 }
                 $value = $this->prefs[$name];
@@ -1292,6 +1297,21 @@ class Preferences
 
         $this->setReplacements($replacements);
 
         return $this;
     }
+
+    /**
+     * Purify HTML value
+     *
+     * @param string $value Value to clean
+     *
+     * @return string
+     */
+    public function cleanHtmlValue(string $value): string
+    {
+        $config = \HTMLPurifier_Config::createDefault();
+        $config->set('Cache.SerializerPath', GALETTE_CACHE_DIR);
+        $purifier = new \HTMLPurifier($config);
+        return $purifier->purify($value);
+    }
 }
diff --git a/galette/lib/Galette/Entity/Entitled.php b/galette/lib/Galette/Entity/Entitled.php
index 20e71edbb..b4671a28d 100644
--- a/galette/lib/Galette/Entity/Entitled.php
+++ b/galette/lib/Galette/Entity/Entitled.php
@@ -427,10 +427,11 @@ abstract class Entitled
      * @return integer id if success ; -1 : DB error ; -2 : label already exists
      */
     public function add($label, $extra)
     {
         // Avoid duplicates.
+        $label = strip_tags($label);
         $ret = $this->getIdByLabel($label);
 
         if ($ret !== false) {
             Analog::log(
                 $this->getType() . ' with label `' . $label . '` already exists',
@@ -487,10 +488,11 @@ abstract class Entitled
      *
      * @return ID_NOT_EXITS|boolean
      */
     public function update($id, $label, $extra)
     {
+        $label = strip_tags($label);
         $ret = $this->get($id);
         if (!$ret) {
             /* get() already logged and set $this->error. */
             return self::ID_NOT_EXITS;
         }
diff --git a/galette/lib/Galette/Entity/Title.php b/galette/lib/Galette/Entity/Title.php
index 3218522f0..dfdc2d174 100644
--- a/galette/lib/Galette/Entity/Title.php
+++ b/galette/lib/Galette/Entity/Title.php
@@ -137,12 +137,12 @@ class Title
      * @return boolean
      */
     public function store($zdb)
     {
         $data = array(
-            'short_label'   => $this->short,
-            'long_label'    => $this->long
+            'short_label'   => strip_tags($this->short),
+            'long_label'    => strip_tags($this->long)
         );
         try {
             if ($this->id !== null && $this->id > 0) {
                 $update = $zdb->update(self::TABLE);
                 $update->set($data)->where([self::PK => $this->id]);
diff --git a/galette/lib/Galette/Entity/Transaction.php b/galette/lib/Galette/Entity/Transaction.php
index 08caee36c..799d5e91a 100644
--- a/galette/lib/Galette/Entity/Transaction.php
+++ b/galette/lib/Galette/Entity/Transaction.php
@@ -355,11 +355,11 @@ class Transaction
                                 $this->errors[] = _T("- The amount must be an integer!");
                             }
                             break;
                         case 'trans_desc':
                             /** TODO: retrieve field length from database and check that */
-                            $this->_description = $value;
+                            $this->_description = strip_tags($value);
                             if (mb_strlen($value) > 150) {
                                 $this->errors[] = _T("- Transaction description must be 150 characters long maximum.");
                             }
                             break;
                     }
diff --git a/galette/templates/default/gestion_intitule_content.tpl b/galette/templates/default/gestion_intitule_content.tpl
index ee9f05a09..a6cfa2a85 100644
--- a/galette/templates/default/gestion_intitule_content.tpl
+++ b/galette/templates/default/gestion_intitule_content.tpl
@@ -55,11 +55,11 @@
             <tr class="{if $smarty.foreach.allentries.iteration % 2 eq 0}even{else}odd{/if}">
                 <td data-scope="row">
                     {$eid}
                     <span class="row-title">
                         <a href="{path_for name="editEntitled" data=["class" => $url_class, "action" => "edit", "id" => $eid]}">
-                            {_T string="%s field" pattern="/%s/" replace=$entry.name}
+                            {_T string="%s field" pattern="/%s/" replace=$entry.name|escape}
                         </a>
                     </span>
                 </td>
                 <td class="left" data-title="{_T string="Name"}">
 
@@ -87,18 +87,18 @@
                     <a
                         href="{path_for name="editEntitled" data=["class" => $url_class, "action" => "edit", "id" => $eid]}"
                         class="action tooltip"
                     >
                         <i class="fas fa-edit fa-fw"></i>
-                        <span class="sr-only">{_T string="Edit '%s' field" pattern="/%s/" replace=$entry.name}</span>
+                        <span class="sr-only">{_T string="Edit '%s' field" pattern="/%s/" replace=$entry.name|escape}</span>
                     </a>
                     <a
                         href="{path_for name="removeEntitled" data=["class" => $url_class, "id" => $eid]}"
                         class="delete tooltip"
                     >
                         <i class="fas fa-trash fa-fw"></i>
-                        <span class="sr-only">{_T string="Delete '%s' field" pattern="/%s/" replace=$entry.name}</span>
+                        <span class="sr-only">{_T string="Delete '%s' field" pattern="/%s/" replace=$entry.name|escape}</span>
                     </a>
                 </td>
             </tr>
 {/foreach}
         </tbody>
diff --git a/galette/templates/default/gestion_titres.tpl b/galette/templates/default/gestion_titres.tpl
index 52ed939c8..762e32dea 100644
--- a/galette/templates/default/gestion_titres.tpl
+++ b/galette/templates/default/gestion_titres.tpl
@@ -42,33 +42,33 @@
     {else}
                                 {$smarty.foreach.alltitles.iteration}
     {/if}
                                 <span class="row-title">
                                     <a href="{path_for name="editTitle" data=["id" => $title->id]}">
-                                        {_T string="%s title" pattern="/%s/" replace=$title->short}
+                                        {_T string="%s title" pattern="/%s/" replace=$title->short|escape}
                                     </a>
                                 </span>
                             </td>
-                            <td class="left" data-title="{_T string="Short form"}">{$title->short}</td>
-                            <td class="left" data-title="{_T string="Long form"}">{$title->long}</td>
+                            <td class="left" data-title="{_T string="Short form"}">{$title->short|escape}</td>
+                            <td class="left" data-title="{_T string="Long form"}">{$title->long|escape}</td>
                             <td class="center actions_row">
                                 <a
                                     href="{path_for name="editTitle" data=["id" => $title->id]}"
                                     class="tooltip action"
                                 >
                                     <i class="fas fa-edit fa-fw"></i>
-                                    <span class="sr-only">{_T string="Edit '%s' title" pattern="/%s/" replace=$title->short}</span>
+                                    <span class="sr-only">{_T string="Edit '%s' title" pattern="/%s/" replace=$title->short|escape}</span>
                                 </a>
                 {if $title->id eq 1 or $title->id eq 2}
                                 <img src="{base_url}/{$template_subdir}images/icon-empty.png" alt="" width="16px" height="16px"/>
                 {else}
                                 <a
                                     href="{path_for name="removeTitle" data=["id" => $title->id]}"
                                     class="delete tooltip"
                                 >
                                     <i class="fa fa-trash fa-fw"></i>
-                                    <span class="sr-only">{_T string="Delete '%s' title" pattern="/%s/" replace=$title->short}</span>
+                                    <span class="sr-only">{_T string="Delete '%s' title" pattern="/%s/" replace=$title->short|escape}</span>
                                 </a>
                 {/if}
                             </td>
                         </tr>
             {/foreach}
diff --git a/galette/templates/default/gestion_transactions.tpl b/galette/templates/default/gestion_transactions.tpl
index b812baf43..69273777e 100644
--- a/galette/templates/default/gestion_transactions.tpl
+++ b/galette/templates/default/gestion_transactions.tpl
@@ -148,11 +148,11 @@
                             >
                                 {if isset($member)}{$member->sname}{else}{memberName id="$mid"}{/if}
                             </a>
                         </td>
                     {/if}
-                    <td class="{$cclass} nowrap" data-title="{_T string="Description"}">{$transaction->description}</td>
+                    <td class="{$cclass} nowrap" data-title="{_T string="Description"}">{$transaction->description|escape}</td>
 {if $login->isAdmin() or $login->isStaff()}
                     <td class="{$cclass}" data-title="{_T string="Originator"}">
     {if $filters->filtre_cotis_adh eq ""}
                         <a href="{path_for name="contributions" data=["type" => "transactions", "option" => "member", "value" => $mid]}">
                             {if isset($member)}{$member->sname}{else}{memberName id="$mid"}{/if}

Vulnerable code:         "symfony/polyfill-php80": "^1.23"
    "content-hash": "abdb30cee5eeb0dad05c159a1c10880d",
 * Copyright © 2019-2020 The Galette Team
 * @copyright 2019-2020 The Galette Team
 * @copyright 2019-2020 The Galette Team
                        $ptype->name,
                        $ptype->name,
                        $ptype->name,
                        $ptype->name,
            'short_label'   => $this->short,
            'long_label'    => $this->long
                            $this->_description = $value;
                            {_T string="%s field" pattern="/%s/" replace=$entry.name}
                        <span class="sr-only">{_T string="Edit '%s' field" pattern="/%s/" replace=$entry.name}</span>
                        <span class="sr-only">{_T string="Delete '%s' field" pattern="/%s/" replace=$entry.name}</span>
                                        {_T string="%s title" pattern="/%s/" replace=$title->short}
                            <td class="left" data-title="{_T string="Short form"}">{$title->short}</td>
                            <td class="left" data-title="{_T string="Long form"}">{$title->long}</td>
                                    <span class="sr-only">{_T string="Edit '%s' title" pattern="/%s/" replace=$title->short}</span>
                                    <span class="sr-only">{_T string="Delete '%s' title" pattern="/%s/" replace=$title->short}</span>
                    <td class="{$cclass} nowrap" data-title="{_T string="Description"}">{$transaction->description}</td>
Fixed code:         "symfony/polyfill-php80": "^1.23",
        "ezyang/htmlpurifier": "^4.13"
    "content-hash": "47d56d47701d9038105bd93cf3b44a02",
        {
            "name": "ezyang/htmlpurifier",
            "version": "v4.13.0",
            "source": {
                "type": "git",
                "url": "https://github.com/ezyang/htmlpurifier.git",
                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
                "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
                "shasum": ""
            },
            "require": {
                "php": ">=5.2"
            },
            "require-dev": {
                "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
            },
            "type": "library",
            "autoload": {
                "psr-0": {
                    "HTMLPurifier": "library/"
                },
                "files": [
                    "library/HTMLPurifier.composer.php"
                ],
                "exclude-from-classmap": [
                    "/library/HTMLPurifier/Language/"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "LGPL-2.1-or-later"
            ],
            "authors": [
                {
                    "name": "Edward Z. Yang",
                    "email": "admin@htmlpurifier.org",
                    "homepage": "http://ezyang.com"
                }
            ],
            "description": "Standards compliant HTML filter written in PHP",
            "homepage": "http://htmlpurifier.org/",
            "keywords": [
                "html"
            ],
            "support": {
                "issues": "https://github.com/ezyang/htmlpurifier/issues",
                "source": "https://github.com/ezyang/htmlpurifier/tree/master"
            },
            "time": "2020-06-29T00:56:53+00:00"
        },
 * Copyright © 2019-2021 The Galette Team
 * @copyright 2019-2021 The Galette Team
 * @copyright 2019-2021 The Galette Team
                        $ptype->getName(),
                        $ptype->getName(),
                        $ptype->getName(),
                        $ptype->getName(),
            case 'pref_footer':
                $value = $this->cleanHtmlValue($value);
                break;
            } elseif ($name == 'pref_footer') {
                return $this->cleanHtmlValue($this->prefs[$name]);

    /**
     * Purify HTML value
     *
     * @param string $value Value to clean
     *
     * @return string
     */
    public function cleanHtmlValue(string $value): string
    {
        $config = \HTMLPurifier_Config::createDefault();
        $config->set('Cache.SerializerPath', GALETTE_CACHE_DIR);
        $purifier = new \HTMLPurifier($config);
        return $purifier->purify($value);
    }
        $label = strip_tags($label);
        $label = strip_tags($label);
            'short_label'   => strip_tags($this->short),
            'long_label'    => strip_tags($this->long)
                            $this->_description = strip_tags($value);
                            {_T string="%s field" pattern="/%s/" replace=$entry.name|escape}
                        <span class="sr-only">{_T string="Edit '%s' field" pattern="/%s/" replace=$entry.name|escape}</span>
                        <span class="sr-only">{_T string="Delete '%s' field" pattern="/%s/" replace=$entry.name|escape}</span>
                                        {_T string="%s title" pattern="/%s/" replace=$title->short|escape}
                            <td class="left" data-title="{_T string="Short form"}">{$title->short|escape}</td>
                            <td class="left" data-title="{_T string="Long form"}">{$title->long|escape}</td>
                                    <span class="sr-only">{_T string="Edit '%s' title" pattern="/%s/" replace=$title->short|escape}</span>
                                    <span class="sr-only">{_T string="Delete '%s' title" pattern="/%s/" replace=$title->short|escape}</span>
                    <td class="{$cclass} nowrap" data-title="{_T string="Description"}">{$transaction->description|escape}</td>