CVE-2021-36755: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Nightscout Web Monitor (aka cgm-remote-monitor) 14.2.2 allows XSS via a crafted X-Forwarded-For header.
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: 6.1
Published: 2021-07-16T00:15Z
Repository: https://github.com/nightscout/cgm-remote-monitor
Language: JavaScript
Files changed: lib/client/adminnotifiesclient.js, lib/server/bootevent.js
Commit date: 2021-06-22 09:59:22 -0700
Commit message: prevent XSS via admin-notifies system

The admin-notifies system takes user input from the X-Forwarded-For by header,
and injects it into html rendered by the browser in the admin-notification
system.  This patch appropriately escapes all such content injected into the
notification drawers.  This patch should prevent XSS attacks designed to
exploit user credentials or other access.  Thanks to Deja Vu Security -
Accenture, and Damien Sudol for reporting.

Diff: diff --git a/lib/client/adminnotifiesclient.js b/lib/client/adminnotifiesclient.js
index e301d6c7..cb308658 100644
--- a/lib/client/adminnotifiesclient.js
+++ b/lib/client/adminnotifiesclient.js
@@ -34,12 +34,14 @@ function init (client, $) {
     });
   }
 
   notifies.updateAdminNotifies();
 
-  function wrapmessage(title, message, count, ago, persistent) {
-    let html = '<hr><p><b>' + title + '</b></p><p class="adminNotifyMessage">' + message + '</p>';
+  function wrapmessage(title, message, count, ago, persistent, seeAlso) {
+    let html = $('<div><div><hr><p><b class="adminNotifyTitle"></b></p><p class="adminNotifyMessage"></p></div><p class="adminNotifyMessageAdditionalInfo"></p></div>');
+    html.find('.adminNotifyTitle').text(title);
+    html.find('.adminNotifyMessage').text(message);
 
     let additional = '';
 
     if (count > 1) additional += client.translate('Event repeated %1 times.', count) + ' ';
     let units = client.translate('minutes');
@@ -49,12 +51,15 @@ function init (client, $) {
       units = client.translate('hours');
     }
     if (ago == 0) { ago = client.translate('less than 1'); }
     if (!persistent && ago) additional += client.translate('Last recorded %1 %2 ago.', ago, units);
 
-    if (additional) html += '<p class="adminNotifyMessageAdditionalInfo">' + additional + '</p>'
-    return html;
+    html.find('.adminNotifyMessageAdditionalInfo').text(additional);
+    if (seeAlso && init.addendums[seeAlso]) {
+      html.append($(init.addendums[seeAlso]));
+    }
+    return html.html( );
   }
 
   notifies.prepare = function prepare() {
 
     var translate = client.translate;
@@ -67,11 +72,11 @@ function init (client, $) {
       html += '<p><b>' + translate('You have administration messages') + '</b></p>';
       for(var i = 0 ; i < messages.length; i++) {
         /* eslint-disable-next-line security/detect-object-injection */ // verified false positive
         var m = messages[i];
         const ago = Math.round((Date.now() - m.lastRecorded) / 60000);
-        html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent);
+        html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent, m.seeAlso);
       }
     } else {
       if (messageCount > 0) {
         html = wrapmessage(translate('Admin messages in queue'), translate('Please sign in using the API_SECRET to see your administration messages'));
       } else {
@@ -97,7 +102,11 @@ function init (client, $) {
   notifies.button.css('color','red');
 
   return notifies;
 
 }
+init.addendums = {
+  disableWorldReadable: '<p>Please consider closing access to the site by following the instructions in the <a href="http://nightscout.github.io/nightscout/security/#how-to-turn-off-unauthorized-access" target="_new">Nightscout documentation</a>.</p>'
+
+};
 
 module.exports = init;
diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js
index 538fe505..31c11490 100644
--- a/lib/server/bootevent.js
+++ b/lib/server/bootevent.js
@@ -119,12 +119,13 @@ function boot (env, language) {
     }
 
     if (env.settings.authDefaultRoles == 'readable') {
       const message = {
         title: "Nightscout readable by world"
-        ,message: "Your Nightscout installation is readable by anyone who knows the web page URL. Please consider closing access to the site by following the instructions in the <a href=\"http://nightscout.github.io/nightscout/security/#how-to-turn-off-unauthorized-access\" target=\"_new\">Nightscout documentation</a>."
+        ,message: "Your Nightscout installation is readable by anyone who knows the web page URL."
         ,persistent: true
+        ,seeAlso: 'disableWorldReadable'
       };
       ctx.adminnotifies.addNotify(message);
     }
 
     next();

Vulnerable code:   function wrapmessage(title, message, count, ago, persistent) {
    let html = '<hr><p><b>' + title + '</b></p><p class="adminNotifyMessage">' + message + '</p>';
    if (additional) html += '<p class="adminNotifyMessageAdditionalInfo">' + additional + '</p>'
    return html;
        html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent);
        ,message: "Your Nightscout installation is readable by anyone who knows the web page URL. Please consider closing access to the site by following the instructions in the <a href=\"http://nightscout.github.io/nightscout/security/#how-to-turn-off-unauthorized-access\" target=\"_new\">Nightscout documentation</a>."
Fixed code:   function wrapmessage(title, message, count, ago, persistent, seeAlso) {
    let html = $('<div><div><hr><p><b class="adminNotifyTitle"></b></p><p class="adminNotifyMessage"></p></div><p class="adminNotifyMessageAdditionalInfo"></p></div>');
    html.find('.adminNotifyTitle').text(title);
    html.find('.adminNotifyMessage').text(message);
    html.find('.adminNotifyMessageAdditionalInfo').text(additional);
    if (seeAlso && init.addendums[seeAlso]) {
      html.append($(init.addendums[seeAlso]));
    }
    return html.html( );
        html += wrapmessage(translate(m.title), translate(m.message), m.count, ago, m.persistent, m.seeAlso);
init.addendums = {
  disableWorldReadable: '<p>Please consider closing access to the site by following the instructions in the <a href="http://nightscout.github.io/nightscout/security/#how-to-turn-off-unauthorized-access" target="_new">Nightscout documentation</a>.</p>'

};
        ,message: "Your Nightscout installation is readable by anyone who knows the web page URL."
        ,seeAlso: 'disableWorldReadable'