CVE-2022-23466: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: teler is an real-time intrusion detection and threat alert dashboard. teler prior to version 2.0.0-rc.4 is vulnerable to DOM-based cross-site scripting (XSS) in the teler dashboard. When teler requests messages from the event stream on the `/events` endpoint, the log data displayed on the dashboard are not sanitized. This only affects authenticated users and can only be exploited based on detected threats if the log contains a DOM scripting payload. This vulnerability has been fixed on version `v2.0.0-rc.4`. Users are advised to upgrade. There are no known workarounds for this vulnerability.
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: 2022-12-06T18:15Z
Repository: https://github.com/kitabisa/teler
Language: JavaScript
Files changed: internal/event/www/script.js
Commit date: 2022-12-04 03:23:18 +0700
Commit message: fix(event) dashboard: prevent DOM-XSS for incoming events

Diff: diff --git a/internal/event/www/script.js b/internal/event/www/script.js
index 6483d05..025b655 100644
--- a/internal/event/www/script.js
+++ b/internal/event/www/script.js
@@ -6,10 +6,14 @@ document.addEventListener('DOMContentLoaded', function(event) {
         matchesCount: document.getElementById('matches-count').getElementsByTagName('span')[0],
         filtersClear: document.getElementById('filters-clear'),
         filtersCount: document.getElementById('filters-count').getElementsByTagName('span')[0]
     };
     const slugify = (value) => value.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
+    const escapeHTML = (string) => {
+        const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
+        return string.replace(/[&<>"']/g, function(m) { return map[m]; });
+    };
     const sort = (list) => {      
         signatures = list.getElementsByTagName("li");
         Array.from(signatures)
             .sort((a, b) => parseInt(b.getElementsByClassName('menu-item')[0].getAttribute('data-badge') || 0) - parseInt(a.getElementsByClassName('menu-item')[0].getAttribute('data-badge') || 0))
             .forEach(li => list.appendChild(li));
@@ -68,13 +72,13 @@ document.addEventListener('DOMContentLoaded', function(event) {
 
         var row = document.getElementById('messages').insertRow(0);
         row.classList.add('log', sigId);
         row.id = eventId;
         row.insertCell(0).innerHTML = `<td class="date"><span class="datetime" title="${new Date().toLocaleString}">${new Date().toLocaleTimeString()}</span></td>`;
-        row.insertCell(1).innerHTML = `<td class="category-name"><strong>${data.category}</strong></td>`;
+        row.insertCell(1).innerHTML = `<td class="category-name"><strong>${escapeHTML(data.category)}</strong></td>`;
         row.insertCell(2).innerHTML = `<td class="element"><div>${data.element}</pre></div></td>`;
-        row.insertCell(3).innerHTML = `<td class="matches"><strong>${data[data.element]}</strong></td>`;
+        row.insertCell(3).innerHTML = `<td class="matches"><strong>${escapeHTML(data[data.element])}</strong></td>`;
         row.insertCell(4).innerHTML = `<td class="log-line"><div><pre>${JSON.stringify(data)}</pre></div></td>`;
         settings.matchesCount.textContent = `${document.getElementsByClassName('log').length} threats`;
 
         if (settings.activeSignatures.length > 0 && !settings.activeSignatures.includes(sigId)) row.style.display = 'none';
     };
@@ -101,11 +105,11 @@ document.addEventListener('DOMContentLoaded', function(event) {
         };
     };
     const createSignature = (name) => {
         var li = document.createElement('li');
         li.id = slugify(name)
-        li.innerHTML = `<a href="#" class="menu-item" title="${name}">${name}</a>`;
+        li.innerHTML = `<a href="#" class="menu-item" title="${escapeHTML(name)}">${escapeHTML(name)}</a>`;
         li.addEventListener('click', (e) => {
             e.preventDefault();
             filterSignature(li);
         });
 

Vulnerable code:         row.insertCell(1).innerHTML = `<td class="category-name"><strong>${data.category}</strong></td>`;
        row.insertCell(3).innerHTML = `<td class="matches"><strong>${data[data.element]}</strong></td>`;
        li.innerHTML = `<a href="#" class="menu-item" title="${name}">${name}</a>`;
Fixed code:     const escapeHTML = (string) => {
        const map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
        return string.replace(/[&<>"']/g, function(m) { return map[m]; });
    };
        row.insertCell(1).innerHTML = `<td class="category-name"><strong>${escapeHTML(data.category)}</strong></td>`;
        row.insertCell(3).innerHTML = `<td class="matches"><strong>${escapeHTML(data[data.element])}</strong></td>`;
        li.innerHTML = `<a href="#" class="menu-item" title="${escapeHTML(name)}">${escapeHTML(name)}</a>`;