CVE-2023-3191 (PHP, published 2023-06-10T09:15Z)
Description: value: Cross-site Scripting (XSS) - Stored in GitHub repository nilsteampassnet/teampass prior to 3.0.9.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
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.
Repository: https://github.com/nilsteampassnet/teampass
Files changed:
includes/js/functions.js
pages/admin.js.php
pages/api.js.php
pages/api.php
pages/backups.js.php
pages/emails.js.php
pages/folders.js.php
pages/folders.php
pages/items.js.php
pages/profile.js.php
pages/roles.js.php
pages/users.js.php
pages/users.php
sources/admin.queries.php
Commit message: 3.0.9

Fix xss vulnerabilities in several text inputs


Vulnerable code:
function fieldSanitizeStep1(
    field,
    bHtml=true,
    bSvg=true,
    bSvgFilters=true,
    text=''
    if (field === undefined ||field === '') {
        return false;
    }
    let string = '';
    text = (text === '') ? $(field).val() : text;
/*
    // Sanitize string
    var tagsToReplace = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        "'" : '&#39;',
        '"' : '&quot;'
    };
    text = text.replace(/[&<>'"]/g, function(tag) {
        return tagsToReplace[tag] || tag;
    });
    */
    // Purify string
    string = DOMPurify.sanitize(
            'label': $('#new_api_key_label').val(),
            label = $(this).prev('input').val(),

            'label': $('#new_api_ip_label').val(),
            'value': label,
                                            <input type="text" placeholder="<?php echo langHdl('label'); ?>" class="col-4 form-control form-control-sm" id="new_api_key_label">
                                <div class="form-group mt-4">
                                            <input type="text" placeholder="<?php echo langHdl('ip'); ?>" class="col-4 form-control" id="new_api_ip_value" data-inputmask="'alias': 'ip'" data-mask>
                                            <input type="text" placeholder="<?php echo langHdl('label'); ?>" class="col-4 form-control ml-2" id="new_api_ip_label">
                    'encryptionKey': $('#onthefly-backup-key').val(),
                                "value": $('#onthefly-backup-key').val(),
                    'encryptionKey': $('#onthefly-restore-key').val(),

    $(document).on('click', '#button-duo-save', function() {
        // Prepare data
        var data = {
            'akey': $('#duo_akey').val(),
            'ikey': $('#duo_ikey').val(),
            'skey': $('#duo_skey').val(),
            'host': $('#duo_host').val(),
        }
        console.log(data);

        // Launch action
        $.post(
            'sources/admin.queries.php', {
                type: 'save_duo_in_sk_file',
                data: prepareExchangedData(JSON.stringify(data), "encode", "<?php echo $_SESSION['key']; ?>"),
                key: '<?php echo $_SESSION['key']; ?>'
            },
            function(data) {
                //decrypt data
                data = decodeQueryReturn(data, '<?php echo $_SESSION['key']; ?>');

                if (data.error === true) {
                    // ERROR
                    toastr.remove();
                    toastr.warning(
                        '<?php echo langHdl('none_selected_text'); ?>',
                        '', {
                            timeOut: 5000,
                            progressBar: true
                        }
                    );
                } else {
                    // Inform user
                    toastr.remove();
                    toastr.success(
                        '<?php echo langHdl('done'); ?>',
                        '', {
                            timeOut: 1000
                        }
                    );
                }
            }
        );
    });


                'title': DOMPurify.sanitize($('#new-title').val()),
                'renewalPeriod': DOMPurify.sanitize($('#new-renewal').val() === '' ? 0 : parseInt($('#new-renewal').val())),
                'icon': DOMPurify.sanitize($('#new-folder-add-icon').val(), {USE_PROFILES: {html: true}}),
                'iconSelected': DOMPurify.sanitize($('#new-folder-add-icon-selected').val(), {USE_PROFILES: {html: true}}),
            '<input type="text" class="form-control clear-me" id="folder-edit-title" value="' + folderTitle + '">' +
            '<input type="text" class="form-control form-folder-control" id="folder-edit-icon" value="'+folderIcon+'">' +
            '<input type="text" class="form-control form-folder-control" id="folder-edit-icon-selected" value="'+folderIconSelection+'">' +
                'title': DOMPurify.sanitize($('#folder-edit-title').val()),
                'renewalPeriod': DOMPurify.sanitize($('#folder-edit-renewal').val()),
                'icon': DOMPurify.sanitize($('#folder-edit-icon').val(), {USE_PROFILES: {html: true}}),
                'iconSelected': DOMPurify.sanitize($('#folder-edit-icon-selected').val(), {USE_PROFILES: {html: true}})
                                    <input type="text" class="form-control clear-me" id="new-title">
                                    <input type="text" class="form-control form-folder-control" id="new-folder-add-icon">
                                    <input type="text" class="form-control form-folder-control" id="new-folder-add-icon-selected">
</div>
        let formLabel = fieldSanitizeStep1('#form-folder-add-label', false, false, false),
            formIcon = fieldSanitizeStep1('#form-folder-add-icon', false, false, false),
            formIconSelected = fieldSanitizeStep1('#form-folder-add-icon-selected', false, false, false);
            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');
                    $('#list-item-row_' + value.item_id).attr('data-next-item-id', prevIdForNextItem.item_id);
                    $('#list-item-row_' + value.item_id).attr('data-next-item-label', value.label);
                        icon_favorite = '<span class="fa-stack fa-clickable item-favourite pointer infotip mr-2" title="<?php echo langHdl('unfavorite'); ?>" data-item-id="' + value.item_id + '" data-item-favourited="1"><i class="fas fa-circle fa-stack-2x"></i><i class="fas fa-star fa-stack-1x fa-inverse text-warning"></i></span>';
                        icon_favorite = '<span class="fa-stack fa-clickable item-favourite pointer infotip mr-2" title="<?php echo langHdl('favorite'); ?>" data-item-id="' + value.item_id + '" data-item-favourited="0"><i class="fas fa-circle fa-stack-2x"></i><i class="far fa-star fa-stack-1x fa-inverse"></i></span>';
                    '<tr class="list-item-row' + (value.canMove === 1 ? ' is-draggable' : '') + '" id="list-item-row_' + value.item_id + '" data-item-edition="' + value.open_edit + '" data-item-id="' + value.item_id + '" data-item-sk="' + value.sk + '" data-item-expired="' + value.expired + '" data-item-rights="' + value.rights + '" data-item-display="' + value.display + '" data-item-open-edit="' + value.open_edit + '" data-item-tree-id="' + value.tree_id + '" data-is-search-result="' + value.is_result_of_search + '" data-label="' + escape(value.label) + '">' +
                    '<span class="list-item-clicktoshow' + (value.rights === 10 ? '' : ' pointer') + '" data-item-id="' + value.item_id + '">' +
                    'item_id' : value.item_id,
        if (debugJavascript === true) console.info('EXPECTED ACTION on ' + itemDefinition + ' is ' + actionType + ' -- ')
                    console.log("PREV: " + $('#list-item-row_'+data.id).prev('.list-item-row').attr('data-item-id') + " - NEXT: " + $('#list-item-row_'+data.id).next('.list-item-row').attr('data-item-id'));
        let formName = fieldSanitizeStep1('#profile-user-name', false, false, false),
            formLastname = fieldSanitizeStep1('#profile-user-lastname', false, false, false),
            formEmail = fieldSanitizeStep1('#profile-user-email', false, false, false);
                'label': DOMPurify.sanitize($('#form-role-label').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;
            }

                    'login': formLogin,
                    'name': formName,
                    'lastname': formLastname,
                    'email': formEmail,
                    'label': DOMPurify.sanitize($('#ldap-new-role-selection').val()),
            'login': DOMPurify.sanitize($('.selected-user').data('user-login')),
            'name': DOMPurify.sanitize($('.selected-user').data('user-name') === '' ? $('#ldap-user-name').val() : $('.selected-user').data('user-name')),
            'lastname': DOMPurify.sanitize($('.selected-user').data('user-lastname')),
            'email': DOMPurify.sanitize($('.selected-user').data('user-email')),
                                    <input type="text" class="form-control clear-me required track-change" id="form-name">
                                    <input type="text" class="form-control clear-me required build-login track-change" id="form-login">
                                    <input type="text" class="form-control clear-me required track-change" id="form-lastname">
                                    <input type="email" class="form-control clear-me required track-change validate-email" id="form-email">
                            <input type="text" class="form-control clear-me mt-1" id="form-special-folder" disabled="true" placeholder="<?php echo langHdl('label'); ?>">
            $post_id = filter_var($dataReceived['id'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
                'id=%i',
            $post_id = filter_var($dataReceived['id'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);

Fixed code:
function simplePurifier(
    text,
    bHtml = false,
    bSvg = false,
    bSvgFilters = false
    return DOMPurify.sanitize(
}

/**
 * Permits to purify the content of a string using domPurify
 * @param {*} field 
 * @param {*} bHtml 
 * @param {*} bSvg 
 * @param {*} bSvgFilters 
 * @param {*} text 
 * @returns bool||string
 */
function fieldDomPurifier(
    field,
    bHtml = false,
    bSvg = false,
    bSvgFilters = false,
    text = ''
)
{
    if (field === undefined ||field === '') {
        return false;
    }
    let string = '';
    text = (text === '') ? $(field).val() : text;

    // Purify string
    string = simplePurifier(text, bHtml, bSvg, bSvgFilters);
    return string;
}

/**
 * Permits to get all fields of a class and purify them
 * @param {*} elementClass 
 * @returns array
 */
function fieldDomPurifierLoop(elementClass)
{
    let purifyStop = false,
        arrFields = [];
    $.each($(elementClass), function(index, element) {
        purifiedField = fieldDomPurifier(
            '#' + $(element).attr('id'), 
            $(element).hasClass('purifyHtml') === true ? true : false,
            $(element).hasClass('purifySvg') === true ? true : false,
            $(element).hasClass('purifySvgFilter') === true ? true : false,
            typeof $(element).data('purify-text') !== undefined ? $(element).data('purify-text') : ''
        );

        if (purifiedField === false) {
            // Label is empty
            toastr.remove();
            toastr.warning(
                'XSS attempt detected. Please remove all special characters from your input.',
                'Error', {
                    timeOut: 5000,
                    progressBar: true
                }
            );
            $('#' + $(element).attr('id')).focus();
            purifyStop = true;
            return {
                'purifyStop' : purifyStop,
                'arrFields' : arrFields
            };
        } else {
            $(element).val(purifiedField);
            arrFields[$(element).data('field')] = purifiedField;
        }
    });
    
    // return
    return {
        'purifyStop' : purifyStop,
        'arrFields' : arrFields
    };
}

/**
 * Permits to purify the content of a string using domPurify
 * @param {*} field 
 * @param {*} bHtml 
 * @param {*} bSvg 
 * @param {*} bSvgFilters 
 * @returns bool||string
 */
function fieldDomPurifierWithWarning(
    field,
    bHtml = false,
    bSvg = false,
    bSvgFilters = false,
)
{
    if (field === undefined || field === '') {
        return false;
    }
    if ($(field).val() === '') {
        return '';
    }
    let string = '';

    // Purify string
    string = simplePurifier($(field).val(), bHtml, bSvg, bSvgFilters);
    
    // Clear field if string is empty and warn user
    if (string === '') {
        toastr.remove();
        toastr.warning(
            'XSS attempt detected. Please remove all special characters from your input.',
            'Error', {
                timeOut: 5000,
                progressBar: true
            }
        );
        $(field).focus();
        return false;
    }

        // Sanitize value
        value = fieldDomPurifierWithWarning('#' + field);
        if (value === false) {
            return false;
        }
        $('#' + field).val(value);

        
        // Sanitize text fields
        purifyRes = fieldDomPurifierLoop('#new_api_key_label');
        if (purifyRes.purifyStop === true) {
            // if purify failed, stop
            return false;
        }
            'label': purifyRes.arrFields['label'], //$('#new_api_key_label').val(),
            label = simplePurifier($(this).prev('input').val()),
        
        // Sanitize text fields
        purifyRes = fieldDomPurifierLoop('#new-api-ip .purify');
        if (purifyRes.purifyStop === true) {
            // if purify failed, stop
            return false;
        }

            'label': purifyRes.arrFields['label'],
            'value': simplePurifier(label),
                                            <input type="text" placeholder="<?php echo langHdl('label'); ?>" class="col-4 form-control form-control-sm purify" id="new_api_key_label" data-field="label">
                                <div class="form-group mt-4" id="new-api-ip">
                                            <input type="text" placeholder="<?php echo langHdl('ip'); ?>" class="col-4 form-control" id="new_api_ip_value" data-inputmask="'alias': 'ip'">
                                            <input type="text" placeholder="<?php echo langHdl('label'); ?>" class="col-4 form-control ml-2 purify" id="new_api_ip_label" data-field="label">
                    'encryptionKey': simplePurifier($('#onthefly-backup-key').val()),
                                "value": simplePurifier($('#onthefly-backup-key').val()),
                    'encryptionKey': simplePurifier($('#onthefly-restore-key').val()),

            // Sanitize text fields
            purifyRes = fieldDomPurifierLoop('#folder-new .purify');
            if (purifyRes.purifyStop === true) {
                // if purify failed, stop
                return false;
            }

                'title': purifyRes.arrFields['title'],
                'renewalPeriod': $('#new-renewal').val() === '' ? 0 : parseInt($('#new-renewal').val()),
                'icon': purifyRes.arrFields['icon'],
                'iconSelected': purifyRes.arrFields['iconSelected'],
            '<input type="text" class="form-control clear-me purify" id="folder-edit-title" data-field="title" value="' + folderTitle + '">' +
            '<input type="text" class="form-control form-folder-control purify" id="folder-edit-icon" data-field="icon" value="'+folderIcon+'">' +
            '<input type="text" class="form-control form-folder-control purify" id="folder-edit-icon-selected" data-field="iconSelected" value="'+folderIconSelection+'">' +
            // Sanitize text fields
            purifyRes = fieldDomPurifierLoop('#table-folders .purify');
            if (purifyRes.purifyStop === true) {
                // if purify failed, stop
                return false;
            }

                'title': purifyRes.arrFields['title'],    //$('#folder-edit-title').val(),
                'renewalPeriod': $('#new-renewal').val() === '' ? 0 : parseInt($('#new-renewal').val()),
                'icon': purifyRes.arrFields['icon'],    //$('#folder-edit-icon').val(),
                'iconSelected': purifyRes.arrFields['iconSelected'],    //$('#folder-edit-icon-selected').val(),
                                    <input type="text" class="form-control clear-me purify" id="new-title" data-field="title">
                                    <input type="text" class="form-control form-folder-control purify" id="new-folder-add-icon" data-field="icon">
                                    <input type="text" class="form-control form-folder-control purify" id="new-folder-add-icon-selected" data-field="iconSelected">
                                </div>
        let formLabel = fieldDomPurifier('#form-folder-add-label', false, false, false),
            formIcon = fieldDomPurifier('#form-folder-add-icon', false, false, false),
            formIconSelected = fieldDomPurifier('#form-folder-add-icon-selected', false, false, false);
                //$(this).hasClass('but-prev-item') === true ? $('#list-item-row_' + $(this).attr('data-prev-item-key')) : $('#list-item-row_' + $(this).attr('data-next-item-key')),
            let formLabel = fieldDomPurifier('#form-item-label', false, false, false),
                formDescription = $('#form-item-description').summernote('code') !== "<p><br></p>" ? fieldDomPurifier('#form-item-description', true, false, false, $('#form-item-description').summernote('code')) : '',
                formEmail = fieldDomPurifier('#form-item-email'),
                formTags = fieldDomPurifier('#form-item-tags'),
                formUrl = fieldDomPurifier('#form-item-url'),
                formIcon = fieldDomPurifier('#form-item-icon');
                    //$('#list-item-row_' + value.item_id).attr('data-next-item-id', prevIdForNextItem.item_id);
                    //$('#list-item-row_' + value.item_id).attr('data-next-item-label', value.label);
                    $('[data-item-key="'+value.item_key+'"]')
                        //.attr('data-next-item-id', prevIdForNextItem.item_id)
                        .attr('data-next-item-key', prevIdForNextItem.item_key)
                        .attr('data-next-item-label', value.label);
                        icon_favorite = '<span class="fa-stack fa-clickable item-favourite pointer infotip mr-2" title="<?php echo langHdl('unfavorite'); ?>" data-item-id="' + value.item_id + '" data-item-key="' + value.item_key + '" data-item-favourited="1"><i class="fas fa-circle fa-stack-2x"></i><i class="fas fa-star fa-stack-1x fa-inverse text-warning"></i></span>';
                        icon_favorite = '<span class="fa-stack fa-clickable item-favourite pointer infotip mr-2" title="<?php echo langHdl('favorite'); ?>" data-item-id="' + value.item_id + '" data-item-key="' + value.item_key + '" data-item-favourited="0"><i class="fas fa-circle fa-stack-2x"></i><i class="far fa-star fa-stack-1x fa-inverse"></i></span>';
                    '<tr class="list-item-row' + (value.canMove === 1 ? ' is-draggable' : '') + '" id="list-item-row_' + value.item_id + '" data-item-key="' + value.item_key + '" data-item-edition="' + value.open_edit + '" data-item-id="' + value.item_id + '" data-item-sk="' + value.sk + '" data-item-expired="' + value.expired + '" data-item-rights="' + value.rights + '" data-item-display="' + value.display + '" data-item-open-edit="' + value.open_edit + '" data-item-tree-id="' + value.tree_id + '" data-is-search-result="' + value.is_result_of_search + '" data-label="' + escape(value.label) + '">' +
                    '<span class="list-item-clicktoshow' + (value.rights === 10 ? '' : ' pointer') + '" data-item-id="' + value.item_id + '" data-item-key="' + value.item_key + '">' +
                    //'item_id' : value.item_id,
                    'item_key' : value.item_key,
        if (debugJavascript === true) {
            console.info('EXPECTED ACTION on ' + itemDefinition + ' is ' + actionType + ' -- ');
            console.log(itemDefinition);
        }
            var itemKey = parseInt($(itemDefinition).data('item-key')) || '';
            var itemKey = itemDefinition || '';

                /*
                dataItemKey = $('[data-item-key="'+data.item_key+'"]');
                if (dataItemKey.prev('.list-item-row').attr('data-item-key') !== undefined) {
                    $('.but-prev-item')
                        .html('<i class="fas fa-arrow-left mr-2"></i>' + unescape(dataItemKey.prev('.list-item-row').attr('data-label')))
                        .attr('data-prev-item-key', dataItemKey.attr('data-item-key'))
                        .removeClass('hidden');
                }
                if (dataItemKey.next('.list-item-row').attr('data-item-key') !== undefined) {
                    $('.but-next-item')
                        .html('<i class="fas fa-arrow-right mr-2"></i>' + unescape(dataItemKey.next('.list-item-row').attr('data-label')))
                        .attr('data-next-item-id', dataItemKey.next('.list-item-row').attr('data-item-id'))
                        .removeClass('hidden');
                }
                */
                    //console.log("PREV: " + dataItemKey.attr('data-item-key') + " - NEXT: " + $('#list-item-row_'+data.id).next('.list-item-row').attr('data-item-id'));
        let formName = fieldDomPurifier('#profile-user-name', false, false, false),
            formLastname = fieldDomPurifier('#profile-user-lastname', false, false, false),
            formEmail = fieldDomPurifier('#profile-user-email', false, false, false);
            // Sanitize value
            value = fieldDomPurifierWithWarning('#form-role-label');
            if (value === false) {
                return false;
            }
            $('#form-role-label').val(value);

                'label': value,
                // Sanitize text fields
                purifyRes = fieldDomPurifierLoop('#form-user .purify');
                if (purifyRes.purifyStop === true) {
                    // if purify failed, stop
                    return false;
                }

                    'login': purifyRes.arrFields['login'],
                    'name': purifyRes.arrFields['name'],
                    'lastname': purifyRes.arrFields['lastname'],
                    'email': purifyRes.arrFields['email'],
                    'label': simplePurifier($('#ldap-new-role-selection').val()),
        // Sanitize text fields
        purifyRes = fieldDomPurifierLoop('#form-user .purify');
        if (purifyRes.purifyStop === true) {
            // if purify failed, stop
            return false;
        }

            'login': simplePurifier($('.selected-user').data('user-login')),
            'name': simplePurifier($('.selected-user').data('user-name') === '' ? $('#ldap-user-name').val() : $('.selected-user').data('user-name')),
            'lastname': simplePurifier($('.selected-user').data('user-lastname')),
            'email': simplePurifier($('.selected-user').data('user-email')),
                                    <input type="text" class="form-control clear-me required track-change purify" id="form-name" data-field="name">
                                    <input type="text" class="form-control clear-me required build-login track-change purify" id="form-login" data-field="login">
                                    <input type="text" class="form-control clear-me required track-change purify" id="form-lastname" data-field="lastname">
                                    <input type="email" class="form-control clear-me required track-change validate-email purify" id="form-email" data-field="email">
                            <input type="text" class="form-control clear-me mt-1 purify" id="form-special-folder" data-field="" disabled="true" placeholder="<?php echo langHdl('label'); ?>">
            $post_id = filter_var($dataReceived['id'], FILTER_SANITIZE_NUMBER_INT);
                'increment_id=%i',
            $post_id = filter_var($dataReceived['id'], FILTER_SANITIZE_NUMBER_INT);