CVE-2023-3531 (PHP, published 2023-07-06T20:15Z)
Description: value: Cross-site Scripting (XSS) - Stored in GitHub repository nilsteampassnet/teampass prior to 3.0.10.
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
install/install.queries.php
install/upgrade_operations.php
install/upgrade_run_3.0.php
install/upgrade_scripts_manager.php
pages/items.js.php
pages/items.php
sources/items.queries.php
Commit message: 3.0.10

Including new time related fields
Improving XSS protection


Vulnerable code:
            typeof $(element).data('purify-text') !== undefined ? $(element).data('purify-text') : ''
            arrFields[$(element).data('field')] = purifiedField;
    // ---->
    // OPERATION - 20230604_1 - generate key for item_key
    // Get items to treat
    $rows = mysqli_query(
        "SELECT id FROM ".$pre."items
        WHERE item_key = '-1'
        ORDER BY id
        LIMIT ".$post_nb.";"
    // Handle error on query
    if (!$rows) {
        echo '[{"finish":"1" , "error":"'.mysqli_error($db_link).'"}]';
        exit();
    // Get total of items to treat
    $total = mysqli_num_rows($rows);
    // Loop on items and update for requested ones
    if ((int) $total > 0) {
        while ($row = mysqli_fetch_array($rows, MYSQLI_ASSOC)) {
            // Gererate a key and update
                "UPDATE `".$pre."items`
                SET `item_key` = '".uniqidReal(50)."'
                WHERE `id` = ".$row['id'].";"
            if (mysqli_error($db_link)) {
                echo '[{"finish":"1", "next":"", "error":"MySQL Error! '.addslashes(mysqli_error($db_link)).'"}]';
                exit();
            }
    // Manage end of operation
    if ($total === 0) {
        $finish = 1;
    } else {
        $finish = 0;
    }
    // ----<
// Close connection
mysqli_close($db_link);

// Return back
echo '[{"finish":"'.$finish.'" , "next":"", "error":"", "total":"'.$total.'"}]';
//
// populate created_at, updated_at and deleted_at fields in USERS table
//---<END 3.0.9
}
            'label': DOMPurify.sanitize($('#form-item-copy-new-label').val()),

            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');
            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
                    }
                );
            if (formLabel === '') {
            } else if (formTags !== '' &&
                reg.test(formTags)
                    'description': formDescription,
                    'email': formEmail,
                    'label': formLabel,
                    'login': DOMPurify.sanitize($('#form-item-login').val()),
                    'tags': formTags,
                    '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': formUrl,
                    'fa_icon': formIcon,
                                    <input id="form-item-label" type="text" class="form-control form-item-control" data-change-ongoing="" data-field-name="label">
                                    <div id="form-item-description" class="form-item-control w-100 clear-me-html" data-field-name="description" data-change-ongoing=""></div>
                                    <input id="form-item-login" type="text" class="form-control form-item-control" data-field-name="login" data-change-ongoing="">
                                    <input id="form-item-email" type="email" class="form-control form-item-control" data-field-name="email" data-change-ongoing="">
                                    <input id="form-item-url" type="url" class="form-control form-item-control" data-field-name="url" data-change-ongoing="">
                                    <input id="form-item-icon" type="text" class="form-control form-item-control" data-field-name="icon" data-change-ongoing="">
                                    <input id="form-item-tags" type="text" class="form-control form-item-control autocomplete" data-change-ongoing="">
                                                    <input type="text" class="form-control form-item-control" id="form-item-deleteAfterShown" data-change-ongoing="">
                                                        <input type="text" class="form-control float-right form-item-control" id="form-item-deleteAfterDate" data-change-ongoing="">
        $inputData['label'] = filter_var($dataReceived['label'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
        // perform a check in case of Read-Only user creating an item in his PF
        if (($_SESSION['user_read_only'] === true
                && in_array($inputData['label'], $_SESSION['personal_folders']) === false)
            || (int) $post_access_level <= 20
        ) {
            echo (string) prepareExchangedData(
                $SETTINGS['cpassman_dir'],
                array(
                    'error' => true,
                    'message' => langHdl('error_not_allowed_to'),
                ),
                'encode'
            );
            break;
        }

$SETTINGS['cpassman_dir'],

Fixed code:
            .replaceAll('&#x3C;', '<')
            .replaceAll('&#60;', '<')
            .replaceAll('&#x3E;', '>')
            .replaceAll('&#62;', '>')
            .replaceAll('&#38;', '&')
            .replaceAll('&#x26;', '&')
            .replaceAll('&#34;;', '"')
            .replaceAll('&#x22;', '"')
            typeof $(element).data('purify-text') !== 'undefined' ? $(element).data('purify-text') : ''
            if (typeof $(element).data('field') !== 'undefined') {
                arrFields[$(element).data('field')] = purifiedField;
            } else if (typeof $(element).data('field-name') !== 'undefined') {
                arrFields[$(element).data('field-name')] = purifiedField;
            }
                            `created_at` varchar(30) NULL,
                            `updated_at` varchar(30) NULL,
                            `deleted_at` varchar(30) NULL,
    if ($post_operation === '20230604_1') {
        // ---->
        // OPERATION - 20230604_1 - generate key for item_key

        // Get items to treat
        $rows = mysqli_query(
            $db_link,
            "SELECT id FROM ".$pre."items
            WHERE item_key = '-1'
            ORDER BY id
            LIMIT ".$post_nb.";"
        );
        // Handle error on query
        if (!$rows) {
            echo '[{"finish":"1" , "error":"'.mysqli_error($db_link).'"}]';
            exit();
        }

        // Get total of items to treat
        $total = mysqli_num_rows($rows);

        // Loop on items and update for requested ones
        if ((int) $total > 0) {
            while ($row = mysqli_fetch_array($rows, MYSQLI_ASSOC)) {
                // Gererate a key and update
                mysqli_query(
                    $db_link,
                    "UPDATE `".$pre."items`
                    SET `item_key` = '".uniqidReal(50)."'
                    WHERE `id` = ".$row['id'].";"
                );
                if (mysqli_error($db_link)) {
                    echo '[{"finish":"1", "next":"", "error":"MySQL Error! '.addslashes(mysqli_error($db_link)).'"}]';
                    exit();
                }
            }
        }

        // Manage end of operation
        if ($total === 0) {
            $finish = 1;
        } else {
            $finish = 0;
        }
        // ----<
    } elseif ($post_operation === 'populateItemsTable_CreatedAt') {
        $finish = populateItemsTable_CreatedAt($pre, $post_nb);
    } elseif ($post_operation === 'populateItemsTable_UpdatedAt') {
        $finish = populateItemsTable_UpdatedAt($pre);
    } elseif ($post_operation === 'populateItemsTable_DeletedAt') {
        $finish = populateItemsTable_DeletedAt($pre);
    }
}
// Close connection
mysqli_close($db_link);


// Return back
echo '[{"finish":"'.$finish.'" , "next":"", "error":"", "total":"'.$total.'"}]';



function populateItemsTable_CreatedAt($pre, $post_nb)
{
    global $db_link;
    // loop on items - created_at
    $items = mysqli_query(
        "select i.id as id, ls.date as datetime
        from `" . $pre . "items` as i
        inner join `" . $pre . "log_items` as ls on ls.id_item = i.id
        WHERE ls.action = 'at_creation' AND i.created_at IS NULL
        LIMIT " . $post_nb.";"
    while ($item = mysqli_fetch_assoc($items)) {
        if (empty((string) $item['datetime']) === false && is_null($item['datetime']) === false) {
            // update created_at field
            mysqli_query(
                $db_link,
                "UPDATE `" . $pre . "items` SET created_at = '".$item['datetime']."' WHERE id = ".$item['id']
            );
        }
    // Is it finished?
    $remainingItems = mysqli_num_rows(
        mysqli_query(
            $db_link,
            "SELECT * FROM `" . $pre . "items` WHERE created_at IS NULL"
        )
    );
    return $remainingItems > 0 ? 0 : 1;
}
function populateItemsTable_UpdatedAt($pre)
{
    global $db_link;
    // loop on items - updated_at
    $items = mysqli_query(
        $db_link,
        "select i.id as id, (select date from " . $pre . "log_items where action = 'at_modification' and id_item=id order by date DESC limit 1) as datetime 
        from `" . $pre . "items` as i;"
    );
    while ($item = mysqli_fetch_assoc($items)) {
        if (is_null($item['datetime']) === false) {
            // update updated_at field
                "UPDATE `" . $pre . "items` SET updated_at = '".$item['datetime']."' WHERE id = ".$item['id']
    return 1;
function populateItemsTable_DeletedAt($pre)
{
    global $db_link;
    // loop on items - deleted_at
    $items = mysqli_query(
        $db_link,
        "select i.id as id, (select date from " . $pre . "log_items where action = 'at_deleted' and id_item=id order by date DESC limit 1) as datetime
        from `" . $pre . "items` as i;"
    );
    while ($item = mysqli_fetch_assoc($items)) {
        if (is_null($item['datetime']) === false) {
            // update updated_at field
            mysqli_query(
                $db_link,
                "UPDATE `" . $pre . "items` SET deleted_at = '".$item['datetime']."' WHERE id = ".$item['id']
            );
        }
    }
    return 1;
}
//---------------------------------------------------------------------

//---------------------------------------------------------------------
//---------------------------------------------------------------------
//---------------------------------------------------------------------
//---------------------------------------------------------------------
//---------------------------------------------------------------------
// populate created_at, updated_at and deleted_at fields in USERS table
//---<END 3.0.9
//---------------------------------------------------------------------
//--->BEGIN 3.0.10
// Add field created_at to ITEMS table
$res = addColumnIfNotExist(
    $pre . 'items',
    'created_at',
    "varchar(30) NULL;"
);
if ($res === false) {
    echo '[{"finish":"1", "msg":"", "error":"An error appears when adding field created_at to table ITEMS! ' . mysqli_error($db_link) . '!"}]';
    mysqli_close($db_link);
    exit();
}

// Add field updated_at to ITEMS table
$res = addColumnIfNotExist(
    $pre . 'items',
    'updated_at',
    "varchar(30) NULL;"
);
if ($res === false) {
    echo '[{"finish":"1", "msg":"", "error":"An error appears when adding field updated_at to table ITEMS! ' . mysqli_error($db_link) . '!"}]';
    mysqli_close($db_link);
    exit();
}

// Add field deleted_at to ITEMS table
$res = addColumnIfNotExist(
    $pre . 'items',
    'deleted_at',
    "varchar(30) NULL;"
);
if ($res === false) {
    echo '[{"finish":"1", "msg":"", "error":"An error appears when adding field deleted_at to table ITEMS! ' . mysqli_error($db_link) . '!"}]';
    mysqli_close($db_link);
    exit();
}
/*
// populate created_at, updated_at and deleted_at fields in ITEMS table
$tmp = mysqli_num_rows(mysqli_query($db_link, "SELECT * FROM `" . $pre . "items` WHERE created_at IS NOT NULL"));
if (intval($tmp) === 0) {
    populateItemsTable($pre);
}*/
//---<END 3.0.10

//---------------------------------------------------------------------
}
    array('upgrade_operations.php', 'populateItemsTable_CreatedAt'),
    array('upgrade_operations.php', 'populateItemsTable_UpdatedAt'),
    array('upgrade_operations.php', 'populateItemsTable_DeletedAt'),



                // Prepare datePicker
                $('#form-item-deleteAfterDate, .datepicker').datepicker({
                    format: '<?php echo str_replace(['Y', 'M'], ['yyyy', 'mm'], $SETTINGS['date_format']); ?>',
                    todayHighlight: true,
                    todayBtn: true,
                    language: '<?php echo isset($_SESSION['user_language_code']) === true ? $_SESSION['user_language_code'] : 'en'; ?>'
                });
        
            purifyRes = fieldDomPurifierLoop('#form-item .purify');
            if (purifyRes.purifyStop === true) {
                // if purify failed, stop
            if (purifyRes.arrFields['label'] === '') {
            } else if (purifyRes.arrFields['tags'] !== '' && reg.test(purifyRes.arrFields['tags'])
                    'description': $('#form-item-description').summernote('code'),
                    'email': purifyRes.arrFields['email'],
                    'label': purifyRes.arrFields['label'],
                    'login': purifyRes.arrFields['login'],
                    'tags': purifyRes.arrFields['tags'],
                    'to_be_deleted_after_date': purifyRes.arrFields['deleteAfterDate'] !== '' ? purifyRes.arrFields['deleteAfterDate'] : '',
                    'to_be_deleted_after_x_views': parseInt(purifyRes.arrFields['deleteAfterShown']) > 0 ? parseInt(purifyRes.arrFields['deleteAfterShown']) : '',
                    'url': purifyRes.arrFields['url'],
                    'fa_icon': purifyRes.arrFields['icon'],
                                    <input id="form-item-label" type="text" class="form-control form-item-control purify" data-change-ongoing="" data-field-name="label">
                                    <div id="form-item-description" class="form-item-control w-100 clear-me-html purify purifyHtml" data-field-name="description" data-change-ongoing=""></div>
                                    <input id="form-item-login" type="text" class="form-control form-item-control purify" data-field-name="login" data-change-ongoing="">
                                    <input id="form-item-email" type="email" class="form-control form-item-control purify" data-field-name="email" data-change-ongoing="">
                                    <input id="form-item-url" type="url" class="form-control form-item-control purify" data-field-name="url" data-change-ongoing="">
                                    <input id="form-item-icon" type="text" class="form-control form-item-control " data-field-name="icon" data-change-ongoing="">
                                    <input id="form-item-tags" type="text" class="form-control form-item-control autocomplete purify" data-change-ongoing="" data-field-name="tags">
                                                    <input type="text" class="form-control form-item-control purify" data-field-name="deleteAfterShown" id="form-item-deleteAfterShown" data-change-ongoing="">
                                                        <input type="text" class="form-control float-right form-item-control purify" data-field-name="deleteAfterDate" id="form-item-deleteAfterDate" data-change-ongoing="">
                                        echo "coucou ".count($_SESSION['item_fields']);
                                        } else {
                                            echo
                                            '<div class="alert alert-info">
                                                <h5><i class="icon fa fa-info mr-3"></i>' . langHdl('information') . '</h5>
                                                ' . langHdl('no_fields') . '
                                            </div>';
                        'created_at' => time(),
                        'updated_at' => time(),
            $aSet['created_at'] = time();
                    'updated_at' => time(),
                                'deleted_at' => time(),
                'deleted_at' => time(),
                                'updated_at' => time(),
                    'updated_at' => time(),
                    'updated_at' => time(),
                    'updated_at' => time(),
                    'updated_at' => time(),
                            'updated_at' => time(),
                            'updated_at' => time(),
                            'updated_at' => time(),
                            'updated_at' => time(),
                        $SETTINGS['cpassman_dir'],
                        'deleted_at' => time(),