CVE-2023-3083: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository nilsteampassnet/teampass prior to 3.0.9.
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: 8.7
Published: 2023-06-03T08:15Z
Repository: https://github.com/nilsteampassnet/teampass
Language: PHP
Files changed: includes/core/load.js.php, includes/core/logout.php, index.php, pages/users.js.php, sources/main.queries.php
Commit date: 2023-06-03 08:13:13 +0200
Commit message: 3.0.9

Fix xss in user form
WIP - session ending handling

Diff: diff --git a/includes/core/load.js.php b/includes/core/load.js.php
index 90046065c5..ccf3178717 100755
--- a/includes/core/load.js.php
+++ b/includes/core/load.js.php
@@ -52,10 +52,24 @@ if (
 <script type="text/javascript">
     var userScrollPosition = 0,
         debugJavascript = true;
     let hourInMinutes = 60;
 
+
+    $(document).ready(function() {
+        //console.log('-- PAGE LOADED --'+$(location).attr('href').includes('?')+" -- "+store.get('teampassUser'));
+        if ($(location).attr('href').includes('?') === true && (store.get('teampassUser') === undefined || parseInt(store.get('teampassUser').user_id) <= 0)) {
+            $(location).attr('href', 'index.php?session=expired');
+            return false;
+        }
+
+        // prevent a resubmit on refresh and back button
+        if (window.history.replaceState) {
+            window.history.replaceState(null, null, window.location.href);
+        }
+    });
+
     /**
     *   Add 1 hour to session duration
     **/
     function IncreaseSessionTime(duration)
     {
diff --git a/includes/core/logout.php b/includes/core/logout.php
index f0d7698f4c..29e48e1b66 100755
--- a/includes/core/logout.php
+++ b/includes/core/logout.php
@@ -93,10 +93,11 @@ echo '
         store.remove("teampassApplication");
         store.remove("teampassSettings");
         store.remove("teampassUser");
         store.remove("teampassItem");
         sessionStorage.clear();
+        localStorage.clear();
         
         setTimeout(function() {
             document.location.href="../../index.php"
         }, 1);
     -->
diff --git a/index.php b/index.php
index b9f44bf7fd..fddce48c4b 100755
--- a/index.php
+++ b/index.php
@@ -24,10 +24,13 @@ declare(strict_types=1);
  * @see       https://www.teampass.net
  */
 
 header('X-XSS-Protection: 1; mode=block');
 header('X-Frame-Options: SameOrigin');
+header("Cache-Control: no-cache, no-store, must-revalidate");
+header("Pragma: no-cache");
+header("Expires: 0");
 // **PREVENTING SESSION HIJACKING**
 // Prevents javascript XSS attacks aimed to steal the session ID
 //ini_set('session.cookie_httponly', 1);
 // **PREVENTING SESSION FIXATION**
 // Session ID cannot be passed through URLs
@@ -216,10 +219,18 @@ if (array_key_exists($get['page'], $utilitiesPages) === true) {
         ) {
             if (window.location.href.indexOf('session_over=true') !== -1) {
                 location.replace('./includes/core/logout.php');
             }
         }
+
+        //history.pushState(null, null, 'index.php?session=expired');
+        /*function preventBack() {
+            window.history.forward(); 
+        }        
+        setTimeout("preventBack()", 0);        
+        window.onunload = function () { null };
+        */
         //]]>
     </script>
 
     <!-- IonIcons -->
     <link rel="stylesheet" href="includes/css/ionicons.min.css">
diff --git a/pages/users.js.php b/pages/users.js.php
index 4649494c80..499435ea4d 100755
--- a/pages/users.js.php
+++ b/pages/users.js.php
@@ -878,10 +878,28 @@ if (checkUser($_SESSION['user_id'], $_SESSION['key'], 'folders', $SETTINGS) ===
                         'value': $(this).val(),
                     });
                 }
             });
 
+            // Sanitize text fields
+            let formLogin = fieldSanitizeStep1('#form-login', false, false, false),
+                formName = fieldSanitizeStep1('#form-name', false, false, false),
+                formLastname = fieldSanitizeStep1('#form-lastname', false, false, false),
+                formEmail = fieldSanitizeStep1('#form-email', false, false, false),
+            if (formLogin === false || formName === false || formLastname === false || formEmail === false) {
+                // Label is empty
+                toastr.remove();
+                toastr.warning(
+                    'XSS attempt detected. Field has been emptied.',
+                    'Error', {
+                        timeOut: 5000,
+                        progressBar: true
+                    }
+                );
+                return false;
+            }
+
             if (arrayQuery.length > 0) {
                 // Now save
                 // get lists
                 var forbidFld = [],
                     authFld = [],
@@ -958,14 +976,14 @@ if (checkUser($_SESSION['user_id'], $_SESSION['key'], 'folders', $SETTINGS) ===
                 );
 
                 //prepare data
                 var data = {
                     'user_id': store.get('teampassApplication').formUserId,
-                    'login': DOMPurify.sanitize($('#form-login').val()),
-                    'name': DOMPurify.sanitize($('#form-name').val()),
-                    'lastname': DOMPurify.sanitize($('#form-lastname').val()),
-                    'email': DOMPurify.sanitize($('#form-email').val()),
+                    'login': formLogin,
+                    'name': formName,
+                    'lastname': formLastname,
+                    'email': formEmail,
                     'admin': $('#privilege-admin').prop('checked'),
                     'manager': $('#privilege-manager').prop('checked'),
                     'hr': $('#privilege-hr').prop('checked'),
                     'read_only': $('#privilege-ro').prop('checked'),
                     'personal_folder': $('#form-create-personal-folder').prop('checked'),
diff --git a/sources/main.queries.php b/sources/main.queries.php
index 65f9a7ef8c..76805a9df4 100755
--- a/sources/main.queries.php
+++ b/sources/main.queries.php
@@ -84,11 +84,11 @@ if (
  * Undocumented function.
  */
 function mainQuery(array $SETTINGS)
 {
     header('Content-type: text/html; charset=utf-8');
-    header('Cache-Control: no-cache, must-revalidate');
+    header('Cache-Control: no-cache');
     error_reporting(E_ERROR);
 
 
     // Includes
     include_once $SETTINGS['cpassman_dir'] . '/includes/language/' . $_SESSION['user']['user_language'] . '.php';

Vulnerable code:                     'login': DOMPurify.sanitize($('#form-login').val()),
                    'name': DOMPurify.sanitize($('#form-name').val()),
                    'lastname': DOMPurify.sanitize($('#form-lastname').val()),
                    'email': DOMPurify.sanitize($('#form-email').val()),
    header('Cache-Control: no-cache, must-revalidate');
Fixed code: 
    $(document).ready(function() {
        //console.log('-- PAGE LOADED --'+$(location).attr('href').includes('?')+" -- "+store.get('teampassUser'));
        if ($(location).attr('href').includes('?') === true && (store.get('teampassUser') === undefined || parseInt(store.get('teampassUser').user_id) <= 0)) {
            $(location).attr('href', 'index.php?session=expired');
            return false;
        }

        // prevent a resubmit on refresh and back button
        if (window.history.replaceState) {
            window.history.replaceState(null, null, window.location.href);
        }
    });

        localStorage.clear();
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");

        //history.pushState(null, null, 'index.php?session=expired');
        /*function preventBack() {
            window.history.forward(); 
        }        
        setTimeout("preventBack()", 0);        
        window.onunload = function () { null };
        */
            // Sanitize text fields
            let formLogin = fieldSanitizeStep1('#form-login', false, false, false),
                formName = fieldSanitizeStep1('#form-name', false, false, false),
                formLastname = fieldSanitizeStep1('#form-lastname', false, false, false),
                formEmail = fieldSanitizeStep1('#form-email', false, false, false),
            if (formLogin === false || formName === false || formLastname === false || formEmail === false) {
                // Label is empty
                toastr.remove();
                toastr.warning(
                    'XSS attempt detected. Field has been emptied.',
                    'Error', {
                        timeOut: 5000,
                        progressBar: true
                    }
                );
                return false;
            }

                    'login': formLogin,
                    'name': formName,
                    'lastname': formLastname,
                    'email': formEmail,
    header('Cache-Control: no-cache');