CVE-2023-1884: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Generic in GitHub repository thorsten/phpmyfaq prior to 3.1.12.
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-04-05T17:15Z
Repository: https://github.com/thorsten/phpmyfaq
Language: PHP
Files changed: phpmyfaq/admin/stopwords.php, phpmyfaq/src/phpMyFAQ/Stopwords.php
Commit date: 2023-03-10 07:24:18 +0100
Commit message: fix: added missing conversion to HTML entities

Diff: diff --git a/phpmyfaq/admin/stopwords.php b/phpmyfaq/admin/stopwords.php
index e5c99ba9e9..889cba3e50 100644
--- a/phpmyfaq/admin/stopwords.php
+++ b/phpmyfaq/admin/stopwords.php
@@ -104,14 +104,14 @@ if ($user->perm->hasPermission($user->getUserId(), 'editconfig')) {
             if (i % maxCols === 0) {
               html += '<tr id="stopwords_group_' + i + '">';
             }
 
             // id attribute is of the format stopword_<id>_<lang>
-            elem_id = buildStopWordInputElemId(data[i].id, data[i].lang);
+            elem_id = buildStopWordInputElemId(data[i].id, escape(data[i].lang));
 
             html += '<td>';
-            html += buildStopWordInputElement(elem_id, data[i].stopword);
+            html += buildStopWordInputElement(elem_id, escape(data[i].stopword));
             html += '</td>';
 
             if (i % maxCols === maxCols - 1) {
               html += '</tr>';
             }
@@ -134,11 +134,11 @@ if ($user->perm->hasPermission($user->getUserId(), 'editconfig')) {
          */
         function buildStopWordInputElement(elementId, stopword) {
           elementId = elementId || buildStopWordInputElemId();
           stopword = stopword || '';
           const attrs = 'onblur="saveStopWord(this.id)" onkeydown="saveStopWordHandleEnter(this.id, event)" onfocus="saveOldValue(this.id)"';
-          return '<input class="form-control form-control-sm" id="' + elementId + '" value="' + stopword + '" ' + attrs + '>';
+          return '<input class="form-control form-control-sm" id="' + elementId + '" value="' + escape(stopword) + '" ' + attrs + '>';
         }
 
         /**
          * Id attribute is of the format stopword_<id>_<lang>
          *
@@ -284,10 +284,25 @@ if ($user->perm->hasPermission($user->getUserId(), 'editconfig')) {
                 loadStopWordsByLang(lang);
               },
             );
           }
         }
+
+        const escape = (text) => {
+          const map = {
+            '&': '&amp;',
+            '<': '&lt;',
+            '>': '&gt;',
+            '"': '&quot;',
+            "'": '&#039;',
+          };
+
+          return text.replace(/[&<>"']/g, (mapped) => {
+            return map[mapped];
+          });
+        };
+
       </script>
     </div>
   </div>
     <?php
 } else {
diff --git a/phpmyfaq/src/phpMyFAQ/Stopwords.php b/phpmyfaq/src/phpMyFAQ/Stopwords.php
index c35f4fc39c..4ef229f13c 100644
--- a/phpmyfaq/src/phpMyFAQ/Stopwords.php
+++ b/phpmyfaq/src/phpMyFAQ/Stopwords.php
@@ -190,11 +190,11 @@ class Stopwords
 
         $stopWords = [];
 
         if ($wordsOnly) {
             while (($row = $this->config->getDb()->fetchObject($result)) == true) {
-                $stopWords[] = $row->stopword;
+                $stopWords[] = Strings::htmlentities($row->stopword);
             }
         } else {
             return $this->config->getDb()->fetchAll($result);
         }
 

Vulnerable code:             elem_id = buildStopWordInputElemId(data[i].id, data[i].lang);
            html += buildStopWordInputElement(elem_id, data[i].stopword);
          return '<input class="form-control form-control-sm" id="' + elementId + '" value="' + stopword + '" ' + attrs + '>';
                $stopWords[] = $row->stopword;
Fixed code:             elem_id = buildStopWordInputElemId(data[i].id, escape(data[i].lang));
            html += buildStopWordInputElement(elem_id, escape(data[i].stopword));
          return '<input class="form-control form-control-sm" id="' + elementId + '" value="' + escape(stopword) + '" ' + attrs + '>';

        const escape = (text) => {
          const map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;',
          };

          return text.replace(/[&<>"']/g, (mapped) => {
            return map[mapped];
          });
        };

                $stopWords[] = Strings::htmlentities($row->stopword);