CVE-2023-3551: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value:  Code Injection in GitHub repository nilsteampassnet/teampass prior to 3.0.10.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 7.2
Published: 2023-07-08T09:15Z
Repository: https://github.com/nilsteampassnet/teampass
Language: PHP
Files changed: includes/js/functions.js, includes/libraries/anti-xss-master/src/voku/helper/AntiXSS.php, pages/2fa.js.php, pages/2fa.php, pages/admin.js.php, pages/users.js.php, sources/logs.datatables.php
Commit date: 2023-07-08 10:02:56 +0200
Commit message: 3.0.10

Fix for #3779, #3775
Fix for improper handling of input in admin setting pages

Diff: diff --git a/includes/js/functions.js b/includes/js/functions.js
index 0340b87ba8..8d06dc008b 100755
--- a/includes/js/functions.js
+++ b/includes/js/functions.js
@@ -393,14 +393,14 @@ if (typeof String.prototype.utf8Decode == 'undefined') {
 function simplePurifier(
     text,
     bHtml = false,
     bSvg = false,
     bSvgFilters = false
-)
+) 
 {
     return DOMPurify.sanitize(
-        text
+        sanitizeDom(text)
             .replaceAll('&lt;', '<')
             .replaceAll('&#x3C;', '<')
             .replaceAll('&#60;', '<')
             .replaceAll('&gt;', '>')
             .replaceAll('&#x3E;', '>')
@@ -513,22 +513,35 @@ function fieldDomPurifierLoop(elementClass)
 function fieldDomPurifierWithWarning(
     field,
     bHtml = false,
     bSvg = false,
     bSvgFilters = false,
+    bSetting = false,
 )
 {
     if (field === undefined || field === '') {
         return false;
     }
     if ($(field).val() === '') {
         return '';
     }
-    let string = '';
+    let string = '',
+        currentString = $(field).val();
+
+    // if bSetting is true, we use the setting value
+    // remove any closing ', string that could corrupt the setting
+    if (bSetting === true) {
+        currentString = currentString.replace(/',/g, '');
+    }
 
     // Purify string
-    string = simplePurifier($(field).val(), bHtml, bSvg, bSvgFilters);
+    string = simplePurifier(
+        sanitizeDom(currentString),
+        bHtml,
+        bSvg,
+        bSvgFilters
+    );
     
     // Clear field if string is empty and warn user
     if (string === '') {
         toastr.remove();
         toastr.warning(
@@ -541,6 +554,14 @@ function fieldDomPurifierWithWarning(
         $(field).focus();
         return false;
     }
 
     return string;
+}
+
+const sanitizeDom = (str) => {
+    const div = document.createElement('div');
+    div.textContent = str;
+    newString = div.innerHTML;
+    div.remove();
+    return newString;
 }
\ No newline at end of file
diff --git a/includes/libraries/anti-xss-master/src/voku/helper/AntiXSS.php b/includes/libraries/anti-xss-master/src/voku/helper/AntiXSS.php
index 5f3b8d8c19..364a476491 100644
--- a/includes/libraries/anti-xss-master/src/voku/helper/AntiXSS.php
+++ b/includes/libraries/anti-xss-master/src/voku/helper/AntiXSS.php
@@ -453,10 +453,11 @@ final class AntiXSS
      */
     public function __construct()
     {
         $this->_initNeverAllowedStr();
         $this->_initNeverAllowedRegex();
+        UTF8::checkForSupport();
     }
 
     /**
      * Compact any exploded words.
      *
diff --git a/pages/2fa.js.php b/pages/2fa.js.php
index 02ae8ae743..8e323c7364 100755
--- a/pages/2fa.js.php
+++ b/pages/2fa.js.php
@@ -53,38 +53,19 @@ if (checkUser($_SESSION['user_id'], $_SESSION['key'], '2fa', $SETTINGS) === fals
 
 
 <script type='text/javascript'>
     //<![CDATA[
 
-    console.log('2FA loaded')
-
-    $(document).on('click', '.generate-key', function() {
-        var size = $(this).data('length'),
-            target = $(this).closest('.input-group').find('input').attr('id');
-
-        $.post(
-            'sources/main.queries.php', {
-                type: 'generate_new_key',
-                type_category: 'action_key',
-                size: size
-            },
-            function(data) {
-                $('#' + target).val(data[0].key);
-            },
-            'json'
-        );
-    })
-
-
     $(document).on('click', '#button-duo-config-check', function() {
-        var data = "{\"ikey\":\"" + sanitizeString($("#duo_ikey").val()) + "\", \"skey\":\"" + sanitizeString($("#duo_skey").val()) + "\", \"host\":\"" + sanitizeString($("#duo_host").val()) + "\"}";
+        toastr
+            .info('<?php echo langHdl('loading_item'); ?> ... <i class="fas fa-circle-notch fa-spin fa-2x"></i>');
 
         // Prepare data
         var data = {
-            'duo_ikey': $('#duo_ikey').val(),
-            'duo_skey': $('#duo_skey').val(),
-            'duo_host': $('#duo_host').val()
+            'duo_ikey': sanitizeString($('#duo_ikey').val()),
+            'duo_skey': sanitizeString($('#duo_skey').val()),
+            'duo_host': sanitizeString($('#duo_host').val())
         }
         console.log(data);
 
         // Launch action
         $.post(
@@ -108,11 +89,11 @@ if (checkUser($_SESSION['user_id'], $_SESSION['key'], '2fa', $SETTINGS) === fals
                         }
                     );
                 } else {
                     // Inform user
                     toastr.remove();
-                    toastr.info(
+                    toastr.success(
                         '<?php echo langHdl('duo-config-check-success'); ?>',
                         '', {
                             timeOut: 5000
                         }
                     );
diff --git a/pages/2fa.php b/pages/2fa.php
index 4c3500faf7..6d101aecb8 100755
--- a/pages/2fa.php
+++ b/pages/2fa.php
@@ -155,11 +155,11 @@ require_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php';
                                         <small class='form-text text-muted'>
                                             <?php echo langHdl('admin_ga_website_name_tip'); ?>
                                         </small>
                                     </div>
                                     <div class="col-3">
-                                        <input type="text" class="form-control form-control-sm" id="ga_website_name" value="<?php echo isset($SETTINGS['ga_website_name']) === true ? $SETTINGS['ga_website_name'] : ''; ?>">
+                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="ga_website_name" value="<?php echo isset($SETTINGS['ga_website_name']) === true ? $SETTINGS['ga_website_name'] : ''; ?>">
                                     </div>
                                 </div>
 
                                 <div class="row mb-2">
                                     <div class="col-9">
@@ -203,27 +203,27 @@ require_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php';
                                 <div class="row mb-2">
                                     <div class="col-5">
                                         <?php echo langHdl('admin_duo_ikey'); ?>
                                     </div>
                                     <div class="col-7">
-                                        <input type="text" class="form-control form-control-sm" id="duo_ikey" value="<?php echo isset($SETTINGS['duo_ikey']) === true ? $SETTINGS['duo_ikey'] : ''; ?>">
+                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_ikey" value="<?php echo isset($SETTINGS['duo_ikey']) === true ? $SETTINGS['duo_ikey'] : ''; ?>">
                                     </div>
                                 </div>
                                 <div class="row mb-2">
                                     <div class="col-5">
                                         <?php echo langHdl('admin_duo_skey'); ?>
                                     </div>
                                     <div class="col-7">
-                                        <input type="text" class="form-control form-control-sm" id="duo_skey" value="<?php echo isset($SETTINGS['duo_skey']) === true ? $SETTINGS['duo_skey'] : ''; ?>">
+                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_skey" value="<?php echo isset($SETTINGS['duo_skey']) === true ? $SETTINGS['duo_skey'] : ''; ?>">
                                     </div>
                                 </div>
                                 <div class="row mb-2">
                                     <div class="col-5">
                                         <?php echo langHdl('admin_duo_host'); ?>
                                     </div>
                                     <div class="col-7">
-                                        <input type="text" class="form-control form-control-sm" id="duo_host" value="<?php echo isset($SETTINGS['duo_host']) === true ? $SETTINGS['duo_host'] : ''; ?>">
+                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_host" value="<?php echo isset($SETTINGS['duo_host']) === true ? $SETTINGS['duo_host'] : ''; ?>">
                                     </div>
                                 </div>
 
                                 <div class="row mb-2">
                                     <button class="btn btn-primary" id="button-duo-config-check">
diff --git a/pages/admin.js.php b/pages/admin.js.php
index 852dab00a9..a0fb8189f8 100755
--- a/pages/admin.js.php
+++ b/pages/admin.js.php
@@ -133,11 +133,11 @@ declare(strict_types=1);
         if (requestRunning === true) {
             return false;
         }
 
         // Sanitize value
-        value = fieldDomPurifierWithWarning('#' + field);
+        value = fieldDomPurifierWithWarning('#' + field, false, false, false, true);
         if (value === false) {
             return false;
         }
         $('#' + field).val(value);
 
diff --git a/pages/users.js.php b/pages/users.js.php
index 365f92ac79..6648d6eebd 100755
--- a/pages/users.js.php
+++ b/pages/users.js.php
@@ -896,11 +896,11 @@ if (checkUser($_SESSION['user_id'], $_SESSION['key'], 'folders', $SETTINGS) ===
                     forbidFld.push($(this).val())
                 });
 
                 // Mandatory?
                 var validated = true,
-                    validEmailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
+                    validEmailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,15})+$/;
                 $('.required').each(function(i, obj) {
                     if ($(this).val() === '' && $(this).hasClass('select2') === false) {
                         $(this).addClass('is-invalid');
                         validated = false;
                     } else if ($('#' + $(this).attr('id') + ' :selected').length === 0 && $(this).hasClass('select2') === true) {
diff --git a/sources/logs.datatables.php b/sources/logs.datatables.php
index 89b028519c..649abf8828 100755
--- a/sources/logs.datatables.php
+++ b/sources/logs.datatables.php
@@ -836,11 +836,11 @@ if (isset($_GET['action']) === true && $_GET['action'] === 'connections') {
     } else {
         $sOutput .= '[] }';
     }
 } elseif (isset($_GET['action']) && $_GET['action'] === 'tasks_in_progress') {
     //Columns name
-    $aColumns = ['increment_id', 'created_at', 'updated_at', 'process_type', 'is_in_progress'];
+    $aColumns = ['p.increment_id', 'p.created_at', 'p.updated_at', 'p.process_type', 'p.is_in_progress'];
     //Ordering
     if (isset($_GET['order'][0]['dir']) === true
         && in_array($_GET['order'][0]['dir'], $aSortTypes) === true
         && isset($_GET['order'][0]['column']) === true
         && (int) $_GET['order'][0]['column'] !== 0
@@ -852,28 +852,28 @@ if (isset($_GET['action']) === true && $_GET['action'] === 'connections') {
     } else {
         $sOrder = 'ORDER BY  '.
             $aColumns[0].' DESC';
     }
 
-    $sWhere = ' WHERE ((finished_at = "")';
+    $sWhere = ' WHERE ((p.finished_at = "")';
     if (isset($_GET['search']['value']) === true && $_GET['search']['value'] !== '') {
         $sWhere .= ' AND (';
         for ($i = 0; $i < count($aColumns); ++$i) {
             $sWhere .= $aColumns[$i]." LIKE '%".filter_var($_GET['search']['value'], FILTER_SANITIZE_FULL_SPECIAL_CHARS)."%' OR ";
         }
         $sWhere = substr_replace($sWhere, '', -3).') ';
     }
     $sWhere .= ') ';
     DB::debugmode(false);
     $iTotal = DB::queryFirstField(
-    'SELECT COUNT(increment_id)
+    'SELECT COUNT(p.increment_id)
         FROM '.prefixTable('processes').' AS p 
         LEFT JOIN '.prefixTable('users').' AS u ON u.id = json_extract(p.arguments, "$[0]")'.
         $sWhere
     );
     $rows = DB::query(
-        'SELECT *
+        'SELECT p.*
             FROM '.prefixTable('processes').' AS p 
             LEFT JOIN '.prefixTable('users').' AS u ON u.id = json_extract(p.arguments, "$[0]")'.
             $sWhere.
             $sOrder.
             $sLimit
@@ -949,18 +949,18 @@ if (isset($_GET['action']) === true && $_GET['action'] === 'connections') {
     }
     $sWhere .= ') ';
     
     DB::debugmode(false);
     $iTotal = DB::queryFirstField(
-        'SELECT COUNT(increment_id)
+        'SELECT COUNT(p.increment_id)
             FROM '.prefixTable('processes').' AS p 
             LEFT JOIN '.prefixTable('users').' AS u ON u.id = json_extract(p.arguments, "$[0]")'.
             $sWhere
     );
 
     $rows = DB::query(
-        'SELECT *
+        'SELECT p.*
             FROM '.prefixTable('processes').' AS p 
             LEFT JOIN '.prefixTable('users').' AS u ON u.id = json_extract(p.arguments, "$[0]")'.
             $sWhere.
             $sOrder.
             $sLimit

Vulnerable code: )
        text
    let string = '';
    string = simplePurifier($(field).val(), bHtml, bSvg, bSvgFilters);
    console.log('2FA loaded')

    $(document).on('click', '.generate-key', function() {
        var size = $(this).data('length'),
            target = $(this).closest('.input-group').find('input').attr('id');

        $.post(
            'sources/main.queries.php', {
                type: 'generate_new_key',
                type_category: 'action_key',
                size: size
            },
            function(data) {
                $('#' + target).val(data[0].key);
            },
            'json'
        );
    })


        var data = "{\"ikey\":\"" + sanitizeString($("#duo_ikey").val()) + "\", \"skey\":\"" + sanitizeString($("#duo_skey").val()) + "\", \"host\":\"" + sanitizeString($("#duo_host").val()) + "\"}";
            'duo_ikey': $('#duo_ikey').val(),
            'duo_skey': $('#duo_skey').val(),
            'duo_host': $('#duo_host').val()
                    toastr.info(
                                        <input type="text" class="form-control form-control-sm" id="ga_website_name" value="<?php echo isset($SETTINGS['ga_website_name']) === true ? $SETTINGS['ga_website_name'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm" id="duo_ikey" value="<?php echo isset($SETTINGS['duo_ikey']) === true ? $SETTINGS['duo_ikey'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm" id="duo_skey" value="<?php echo isset($SETTINGS['duo_skey']) === true ? $SETTINGS['duo_skey'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm" id="duo_host" value="<?php echo isset($SETTINGS['duo_host']) === true ? $SETTINGS['duo_host'] : ''; ?>">
        value = fieldDomPurifierWithWarning('#' + field);
                    validEmailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
    $aColumns = ['increment_id', 'created_at', 'updated_at', 'process_type', 'is_in_progress'];
    $sWhere = ' WHERE ((finished_at = "")';
    'SELECT COUNT(increment_id)
        'SELECT *
        'SELECT COUNT(increment_id)
        'SELECT *
Fixed code: ) 
        sanitizeDom(text)
    bSetting = false,
    let string = '',
        currentString = $(field).val();

    // if bSetting is true, we use the setting value
    // remove any closing ', string that could corrupt the setting
    if (bSetting === true) {
        currentString = currentString.replace(/',/g, '');
    }
    string = simplePurifier(
        sanitizeDom(currentString),
        bHtml,
        bSvg,
        bSvgFilters
    );
}

const sanitizeDom = (str) => {
    const div = document.createElement('div');
    div.textContent = str;
    newString = div.innerHTML;
    div.remove();
    return newString;
        UTF8::checkForSupport();
        toastr
            .info('<?php echo langHdl('loading_item'); ?> ... <i class="fas fa-circle-notch fa-spin fa-2x"></i>');
            'duo_ikey': sanitizeString($('#duo_ikey').val()),
            'duo_skey': sanitizeString($('#duo_skey').val()),
            'duo_host': sanitizeString($('#duo_host').val())
                    toastr.success(
                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="ga_website_name" value="<?php echo isset($SETTINGS['ga_website_name']) === true ? $SETTINGS['ga_website_name'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_ikey" value="<?php echo isset($SETTINGS['duo_ikey']) === true ? $SETTINGS['duo_ikey'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_skey" value="<?php echo isset($SETTINGS['duo_skey']) === true ? $SETTINGS['duo_skey'] : ''; ?>">
                                        <input type="text" class="form-control form-control-sm purify" data-field="label" id="duo_host" value="<?php echo isset($SETTINGS['duo_host']) === true ? $SETTINGS['duo_host'] : ''; ?>">
        value = fieldDomPurifierWithWarning('#' + field, false, false, false, true);
                    validEmailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,15})+$/;
    $aColumns = ['p.increment_id', 'p.created_at', 'p.updated_at', 'p.process_type', 'p.is_in_progress'];
    $sWhere = ' WHERE ((p.finished_at = "")';
    'SELECT COUNT(p.increment_id)
        'SELECT p.*
        'SELECT COUNT(p.increment_id)
        'SELECT p.*