CVE-2023-49276: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Uptime Kuma is an open source self-hosted monitoring tool. In affected versions the Google Analytics element in vulnerable to Attribute Injection leading to Cross-Site-Scripting (XSS). Since the custom status interface can set an independent Google Analytics ID and the template has not been sanitized, there is an attribute injection vulnerability here, which can lead to XSS attacks. This vulnerability has been addressed in commit `f28dccf4e` which is included in release version 1.23.7. 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: 6.1
Published: 2023-12-01T22:15Z
Repository: https://github.com/louislam/uptime-kuma
Language: JSON
Files changed: package-lock.json, package.json, server/google-analytics.js
Commit date: 2023-11-24 17:18:01 +0800
Commit message: Merge pull request from GHSA-v4v2-8h88-65qj


Diff: diff --git a/package-lock.json b/package-lock.json
index 81bfcf76..8cf653a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,10 +31,11 @@
                 "express": "~4.17.3",
                 "express-basic-auth": "~1.2.1",
                 "express-static-gzip": "~2.1.7",
                 "form-data": "~4.0.0",
                 "gamedig": "~4.1.0",
+                "html-escaper": "^3.0.3",
                 "http-graceful-shutdown": "~3.1.7",
                 "http-proxy-agent": "~5.0.0",
                 "https-proxy-agent": "~5.0.1",
                 "iconv-lite": "~0.6.3",
                 "isomorphic-ws": "^5.0.0",
@@ -10745,14 +10746,13 @@
             "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
             "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
             "dev": true
         },
         "node_modules/html-escaper": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
-            "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
-            "dev": true
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+            "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
         },
         "node_modules/html-tags": {
             "version": "3.3.1",
             "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
             "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
@@ -11556,10 +11556,16 @@
             },
             "engines": {
                 "node": ">=8"
             }
         },
+        "node_modules/istanbul-reports/node_modules/html-escaper": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+            "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+            "dev": true
+        },
         "node_modules/jackspeak": {
             "version": "2.3.6",
             "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
             "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
             "dependencies": {
diff --git a/package.json b/package.json
index b6c7c22d..cb108bd3 100644
--- a/package.json
+++ b/package.json
@@ -98,10 +98,11 @@
         "express": "~4.17.3",
         "express-basic-auth": "~1.2.1",
         "express-static-gzip": "~2.1.7",
         "form-data": "~4.0.0",
         "gamedig": "~4.1.0",
+        "html-escaper": "^3.0.3",
         "http-graceful-shutdown": "~3.1.7",
         "http-proxy-agent": "~5.0.0",
         "https-proxy-agent": "~5.0.1",
         "iconv-lite": "~0.6.3",
         "isomorphic-ws": "^5.0.0",
diff --git a/server/google-analytics.js b/server/google-analytics.js
index fc9fbec8..3e8e645f 100644
--- a/server/google-analytics.js
+++ b/server/google-analytics.js
@@ -1,23 +1,27 @@
 const jsesc = require("jsesc");
+const { escape } = require("html-escaper");
 
 /**
  * Returns a string that represents the javascript that is required to insert the Google Analytics scripts
  * into a webpage.
  * @param tagId Google UA/G/AW/DC Property ID to use with the Google Analytics script.
  * @returns {string}
  */
 function getGoogleAnalyticsScript(tagId) {
-    let escapedTagId = jsesc(tagId, { isScriptContext: true });
+    let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
 
-    if (escapedTagId) {
-        escapedTagId = escapedTagId.trim();
+    if (escapedTagIdJS) {
+        escapedTagIdJS = escapedTagIdJS.trim();
     }
 
+    // Escape the tag ID for use in an HTML attribute.
+    let escapedTagIdHTMLAttribute = escape(tagId);
+
     return `
-        <script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
-        <script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
+        <script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
+        <script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
     `;
 }
 
 module.exports = {
     getGoogleAnalyticsScript,

Vulnerable code:             "version": "2.0.2",
            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
            "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
            "dev": true
    let escapedTagId = jsesc(tagId, { isScriptContext: true });
    if (escapedTagId) {
        escapedTagId = escapedTagId.trim();
        <script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
        <script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
Fixed code:                 "html-escaper": "^3.0.3",
            "version": "3.0.3",
            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
            "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
        "node_modules/istanbul-reports/node_modules/html-escaper": {
            "version": "2.0.2",
            "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
            "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
            "dev": true
        },
        "html-escaper": "^3.0.3",
const { escape } = require("html-escaper");
    let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
    if (escapedTagIdJS) {
        escapedTagIdJS = escapedTagIdJS.trim();
    // Escape the tag ID for use in an HTML attribute.
    let escapedTagIdHTMLAttribute = escape(tagId);

        <script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
        <script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>