CVE-2023-3009: 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: 5.4
Published: 2023-05-31T13:15Z
Repository: https://github.com/nilsteampassnet/teampass
Language: PHP
Files changed: includes/js/functions.js, pages/items.js.php
Commit date: 2023-05-31 13:20:19 +0200
Commit message: 3.0.9

Vulnerability fix (#3741)

Diff: diff --git a/includes/js/functions.js b/includes/js/functions.js
index 59bd790d11..6f395f954e 100755
--- a/includes/js/functions.js
+++ b/includes/js/functions.js
@@ -386,6 +386,37 @@ if (typeof String.prototype.utf8Decode == 'undefined') {
             return decodeURIComponent( escape( this ) );
         } catch (e) {
             return this; // invalid UTF-8? return as-is
         }
     };
+}
+
+function fieldSanitizeStep1(
+    field,
+    bHtml=true,
+    bSvg=true,
+    bSvgFilters=true,
+    text=''
+)
+{
+    if (field === undefined ||field === '') {
+        return false;
+    }
+    let string = '';
+    text = (text === '') ? $(field).val() : text;
+    
+    // Purify string
+    string = DOMPurify.sanitize(
+        text
+            .replaceAll('&lt;', '<')
+            .replaceAll('&gt;', '>'),
+        {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
+    );
+    
+    // Clear field if string is empty and warn user
+    if (string === '' && text !== '') {
+        $(field).val('');
+        return false;
+    }
+
+    return string;
 }
\ No newline at end of file
diff --git a/pages/items.js.php b/pages/items.js.php
index 3d76a02955..af3f2ff8d9 100755
--- a/pages/items.js.php
+++ b/pages/items.js.php
@@ -2651,28 +2651,50 @@ $var['hidden_asterisk'] = '<i class="fas fa-asterisk mr-2"></i><i class="fas fa-
         }
         if (itemsList.length > 0) {
             userItemRight = itemsList[store.get('teampassItem').id]?.rights;
         }
 
+        
+
         // Do checks
         if (arrayQuery.length > 0 || userDidAChange === true) {
             var reg = new RegExp("[.|,|;|:|!|=|+|-|*|/|#|\"|'|&]");
 
+            // Sanitize text fields
+            let formLabel = fieldSanitizeStep1('#form-item-label', false, false, false),
+                formDescription = $('#form-item-description').summernote('code') !== "<p><br></p>" ? fieldSanitizeStep1('#form-item-description', true, false, false, $('#form-item-description').summernote('code')) : '',
+                formEmail = fieldSanitizeStep1('#form-item-email'),
+                formTags = fieldSanitizeStep1('#form-item-tags'),
+                formUrl = fieldSanitizeStep1('#form-item-url'),
+                formIcon = fieldSanitizeStep1('#form-item-icon');
+            if (formLabel === false || formDescription === false || formEmail === false || formTags === false || formUrl === false || formIcon === false) {
+                // Label is empty
+                toastr.remove();
+                toastr.warning(
+                    'XSS attempt detected. Field has been emptied.',
+                    'Error', {
+                        timeOut: 5000,
+                        progressBar: true
+                    }
+                );
+                return false;
+            }
+            
             // Do some easy checks
-            if ($('#form-item-label').val() === '') {
+            if (formLabel === '') {
                 // Label is empty
                 toastr.remove();
                 toastr.error(
                     '<?php echo langHdl('error_label'); ?>',
                     '', {
                         timeOut: 5000,
                         progressBar: true
                     }
                 );
                 return false;
-            } else if ($('#form-item-tags').val() !== '' &&
-                reg.test($('#form-item-tags').val())
+            } else if (formTags !== '' &&
+                reg.test(formTags)
             ) {
                 // Tags not wel formated
                 toastr.remove();
                 toastr.error(
                     '<?php echo langHdl('error_tags'); ?>',
@@ -2788,43 +2810,45 @@ $var['hidden_asterisk'] = '<i class="fas fa-asterisk mr-2"></i><i class="fas fa-
                             }
                         );
                     }
                     return false;
                 }
+                    
                 //prepare data
                 var data = {
                     'anyone_can_modify': $('#form-item-anyoneCanModify').is(':checked') ? 1 : 0,
                     'complexity_level': parseInt($('#form-item-password-complex').val()),
-                    'description': $('#form-item-description').summernote('code') !== "<p><br></p>" ? DOMPurify.sanitize($('#form-item-description').summernote('code'), {USE_PROFILES: {html: true, svg: false, svgFilters: false}}) : '',
+                    'description': formDescription,
                     'diffusion_list': diffusion,
                     'diffusion_list_names': diffusionNames,
                     'folder': parseInt($('#form-item-folder').val()),
-                    'email': DOMPurify.sanitize($('#form-item-email').val()),
+                    'email': formEmail,
                     'fields': fields,
                     'folder_is_personal': store.get('teampassItem').IsPersonalFolder === 1 ? 1 : 0,
                     'id': store.get('teampassItem').id,
-                    'label': DOMPurify.sanitize($('#form-item-label').val(), {USE_PROFILES: {html:false, svg: false, svgFilters: false}}),
+                    'label': formLabel,
                     'login': DOMPurify.sanitize($('#form-item-login').val()),
                     'pw': $('#form-item-password').val(),
                     'restricted_to': restriction,
                     'restricted_to_roles': restrictionRole,
-                    'tags': DOMPurify.sanitize($('#form-item-tags').val()),
+                    'tags': formTags,
                     'template_id': parseInt($('input.form-check-input-template:checkbox:checked').data('category-id')),
                     'to_be_deleted_after_date': ($('#form-item-deleteAfterDate').length !== 0 &&
                         $('#form-item-deleteAfterDate').val() !== '') ? $('#form-item-deleteAfterDate').val() : '',
                     'to_be_deleted_after_x_views': ($('#form-item-deleteAfterShown').length !== 0 &&
                             $('#form-item-deleteAfterShown').val() !== '' && $('#form-item-deleteAfterShown').val() >= 1) ?
                         parseInt($('#form-item-deleteAfterShown').val()) : '',
-                    'url': DOMPurify.sanitize($('#form-item-url').val()),
+                    'url': formUrl,
                     'user_id': parseInt('<?php echo $_SESSION['user_id']; ?>'),
                     'uploaded_file_id': store.get('teampassApplication').uploadedFileId === undefined ? '' : store.get('teampassApplication').uploadedFileId,
-                    'fa_icon': DOMPurify.sanitize($('#form-item-icon').val()),
+                    'fa_icon': formIcon,
                 };
                 if (debugJavascript === true) {
                     console.log('SAVING DATA');
                     console.log(data);
                 }
+
                 // Inform user
                 toastr.remove();
                 toastr.info(
                     '<?php echo langHdl('opening_folder'); ?><i class="fas fa-circle-notch fa-spin ml-2"></i>'
                 );

Vulnerable code:             if ($('#form-item-label').val() === '') {
            } else if ($('#form-item-tags').val() !== '' &&
                reg.test($('#form-item-tags').val())
                    'description': $('#form-item-description').summernote('code') !== "<p><br></p>" ? DOMPurify.sanitize($('#form-item-description').summernote('code'), {USE_PROFILES: {html: true, svg: false, svgFilters: false}}) : '',
                    'email': DOMPurify.sanitize($('#form-item-email').val()),
                    'label': DOMPurify.sanitize($('#form-item-label').val(), {USE_PROFILES: {html:false, svg: false, svgFilters: false}}),
                    'tags': DOMPurify.sanitize($('#form-item-tags').val()),
                    'url': DOMPurify.sanitize($('#form-item-url').val()),
                    'fa_icon': DOMPurify.sanitize($('#form-item-icon').val()),
Fixed code: }

function fieldSanitizeStep1(
    field,
    bHtml=true,
    bSvg=true,
    bSvgFilters=true,
    text=''
)
{
    if (field === undefined ||field === '') {
        return false;
    }
    let string = '';
    text = (text === '') ? $(field).val() : text;
    
    // Purify string
    string = DOMPurify.sanitize(
        text
            .replaceAll('&lt;', '<')
            .replaceAll('&gt;', '>'),
        {USE_PROFILES: {html:bHtml, svg:bSvg, svgFilters: bSvgFilters}}
    );
    
    // Clear field if string is empty and warn user
    if (string === '' && text !== '') {
        $(field).val('');
        return false;
    }

    return string;
        

            // Sanitize text fields
            let formLabel = fieldSanitizeStep1('#form-item-label', false, false, false),
                formDescription = $('#form-item-description').summernote('code') !== "<p><br></p>" ? fieldSanitizeStep1('#form-item-description', true, false, false, $('#form-item-description').summernote('code')) : '',
                formEmail = fieldSanitizeStep1('#form-item-email'),
                formTags = fieldSanitizeStep1('#form-item-tags'),
                formUrl = fieldSanitizeStep1('#form-item-url'),
                formIcon = fieldSanitizeStep1('#form-item-icon');
            if (formLabel === false || formDescription === false || formEmail === false || formTags === false || formUrl === false || formIcon === false) {
                // Label is empty
                toastr.remove();
                toastr.warning(
                    'XSS attempt detected. Field has been emptied.',
                    'Error', {
                        timeOut: 5000,
                        progressBar: true
                    }
                );
                return false;
            }
            
            if (formLabel === '') {
            } else if (formTags !== '' &&
                reg.test(formTags)
                    
                    'description': formDescription,
                    'email': formEmail,
                    'label': formLabel,
                    'tags': formTags,
                    'url': formUrl,
                    'fa_icon': formIcon,