From 92d22b3fc1f16a4163e822f32b7728015978e1e0 Mon Sep 17 00:00:00 2001
From: KalleV <kvirtaneva@gmail.com>
Date: Fri, 25 Aug 2023 07:11:18 -0400
Subject: [PATCH] fix(cve-2023-29827): replace EJS with Handlebars to resolve
 security warning

Relates to: https://github.com/loopbackio/loopback-next/issues/9867

Signed-off-by: KalleV <kvirtaneva@gmail.com>
---
 lib/send-html.js               |  52 ++++++++++++-
 package-lock.json              | 131 +++++++++++++++++----------------
 package.json                   |   2 +-
 test/handler.test.js           |   9 ++-
 views/default-error.ejs        |  25 -------
 views/default-error.hbs        |  25 +++++++
 views/{style.css => style.hbs} |   0
 7 files changed, 146 insertions(+), 98 deletions(-)
 delete mode 100644 views/default-error.ejs
 create mode 100644 views/default-error.hbs
 rename views/{style.css => style.hbs} (100%)

diff --git a/lib/send-html.js b/lib/send-html.js
index 4f9dc972..02b2adbd 100644
--- a/lib/send-html.js
+++ b/lib/send-html.js
@@ -4,7 +4,7 @@
 // License text available at https://opensource.org/licenses/MIT
 
 'use strict';
-const ejs = require('ejs');
+const handlebars = require('handlebars');
 const fs = require('fs');
 const path = require('path');
 
@@ -16,6 +16,13 @@ const compiledTemplates = {
 
 module.exports = sendHtml;
 
+/**
+ * Sends HTML response to the client.
+ *
+ * @param {Object} res - The response object.
+ * @param {Object} data - The data object to be rendered in the HTML.
+ * @param {Object} options - The options object.
+ */
 function sendHtml(res, data, options) {
   const toRender = {options, data};
   // TODO: ability to call non-default template functions from options
@@ -23,6 +30,35 @@ function sendHtml(res, data, options) {
   sendResponse(res, body);
 }
 
+/**
+ * Returns the content of a Handlebars partial file as a string.
+ * @param {string} name - The name of the Handlebars partial file.
+ * @returns {string} The content of the Handlebars partial file as a string.
+ */
+function partial(name) {
+  const partialPath = path.resolve(assetDir, `${name}.hbs`);
+  const partialContent = fs.readFileSync(partialPath, 'utf8');
+  return partialContent;
+}
+
+handlebars.registerHelper('partial', partial);
+
+/**
+ * Checks if the given property is a standard property.
+ * @param {string} prop - The property to check.
+ * @param {Object} options - The Handlebars options object.
+ * @returns {string} - The result of the Handlebars template.
+ */
+function standardProps(prop, options) {
+  const standardProps = ['name', 'statusCode', 'message', 'stack'];
+  if (standardProps.indexOf(prop) === -1) {
+    return options.fn(this);
+  }
+  return options.inverse(this);
+}
+
+handlebars.registerHelper('standardProps', standardProps);
+
 /**
  * Compile and cache the file with the `filename` key in options
  *
@@ -32,15 +68,23 @@ function sendHtml(res, data, options) {
 function compileTemplate(filepath) {
   const options = {cache: true, filename: filepath};
   const fileContent = fs.readFileSync(filepath, 'utf8');
-  return ejs.compile(fileContent, options);
+  return handlebars.compile(fileContent, options);
 }
 
-// loads and cache default error templates
+/**
+ * Loads the default error handlebars template from the asset directory and compiles it.
+ * @returns {Function} The compiled handlebars template function.
+ */
 function loadDefaultTemplates() {
-  const defaultTemplate = path.resolve(assetDir, 'default-error.ejs');
+  const defaultTemplate = path.resolve(assetDir, 'default-error.hbs');
   return compileTemplate(defaultTemplate);
 }
 
+/**
+ * Sends an HTML response with the given body to the provided response object.
+ * @param {Object} res - The response object to send the HTML response to.
+ * @param {string} body - The HTML body to send in the response.
+ */
 function sendResponse(res, body) {
   res.setHeader('Content-Type', 'text/html; charset=utf-8');
   res.end(body);
diff --git a/package-lock.json b/package-lock.json
index 1840f98b..aff88b7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,8 +11,8 @@
       "dependencies": {
         "accepts": "^1.3.8",
         "debug": "^4.3.4",
-        "ejs": "^3.1.9",
         "fast-safe-stringify": "^2.1.1",
+        "handlebars": "^4.7.8",
         "http-status": "^1.6.2",
         "js2xmlparser": "^5.0.0",
         "strong-globalize": "^6.0.6"
@@ -350,6 +350,7 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
       "dependencies": {
         "color-convert": "^2.0.1"
       },
@@ -406,11 +407,6 @@
         "node": "*"
       }
     },
-    "node_modules/async": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
-      "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
-    },
     "node_modules/asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -570,6 +566,7 @@
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
       "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
       "dependencies": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -657,6 +654,7 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
       "dependencies": {
         "color-name": "~1.1.4"
       },
@@ -667,7 +665,8 @@
     "node_modules/color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
     },
     "node_modules/combined-stream": {
       "version": "1.0.8",
@@ -900,20 +899,6 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
       "dev": true
     },
-    "node_modules/ejs": {
-      "version": "3.1.9",
-      "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
-      "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
-      "dependencies": {
-        "jake": "^10.8.5"
-      },
-      "bin": {
-        "ejs": "bin/cli.js"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1261,33 +1246,6 @@
         "node": "^10.12.0 || >=12.0.0"
       }
     },
-    "node_modules/filelist": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
-      "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
-      "dependencies": {
-        "minimatch": "^5.0.1"
-      }
-    },
-    "node_modules/filelist/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/filelist/node_modules/minimatch": {
-      "version": "5.1.6",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
-      "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1556,6 +1514,26 @@
       "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
       "dev": true
     },
+    "node_modules/handlebars": {
+      "version": "4.7.8",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+      "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
     "node_modules/has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -1572,6 +1550,7 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
       "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
       "engines": {
         "node": ">=8"
       }
@@ -1841,23 +1820,6 @@
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
     },
-    "node_modules/jake": {
-      "version": "10.8.7",
-      "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
-      "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
-      "dependencies": {
-        "async": "^3.2.3",
-        "chalk": "^4.0.2",
-        "filelist": "^1.0.4",
-        "minimatch": "^3.1.2"
-      },
-      "bin": {
-        "jake": "bin/cli.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/js-yaml": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2090,6 +2052,14 @@
         "node": "*"
       }
     },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/mkdirp": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -2214,6 +2184,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2775,6 +2750,14 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
     },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -2909,6 +2892,7 @@
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
       "dependencies": {
         "has-flag": "^4.0.0"
       },
@@ -2989,6 +2973,18 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/uglify-js": {
+      "version": "3.17.4",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+      "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+      "optional": true,
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3039,6 +3035,11 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+    },
     "node_modules/workerpool": {
       "version": "6.2.1",
       "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
diff --git a/package.json b/package.json
index 02134fcc..21ee8ad4 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,8 @@
   "dependencies": {
     "accepts": "^1.3.8",
     "debug": "^4.3.4",
-    "ejs": "^3.1.9",
     "fast-safe-stringify": "^2.1.1",
+    "handlebars": "^4.7.8",
     "http-status": "^1.6.2",
     "js2xmlparser": "^5.0.0",
     "strong-globalize": "^6.0.6"
diff --git a/test/handler.test.js b/test/handler.test.js
index 2bdb92f0..9e502de8 100644
--- a/test/handler.test.js
+++ b/test/handler.test.js
@@ -607,10 +607,12 @@ describe('strong-error-handler', function() {
             expect(res.statusCode).to.eql(404);
             const body = res.error.text;
             expect(body).to.match(
-              /<title>Error&lt;img onerror=alert\(1\) src=a&gt;<\/title>/,
+              // eslint-disable-next-line max-len
+              /<title>Error&lt;img onerror&#x3D;alert\(1\) src&#x3D;a&gt;<\/title>/,
             );
             expect(body).to.match(
-              /with id &lt;img onerror=alert\(1\) src=a&gt; found for Model/,
+              // eslint-disable-next-line max-len
+              /with id &lt;img onerror&#x3D;alert\(1\) src&#x3D;a&gt; found for Model/,
             );
             done();
           });
@@ -627,7 +629,8 @@ describe('strong-error-handler', function() {
           .expect(500)
           .expect(/<title>ErrorWithProps<\/title>/)
           .expect(
-            /500(.*?)a test error message&lt;img onerror=alert\(1\) src=a&gt;/,
+            // eslint-disable-next-line max-len
+            /500(.*?)a test error message&lt;img onerror&#x3D;alert\(1\) src&#x3D;a&gt;/,
             done,
           );
       });
diff --git a/views/default-error.ejs b/views/default-error.ejs
deleted file mode 100644
index 4861d530..00000000
--- a/views/default-error.ejs
+++ /dev/null
@@ -1,25 +0,0 @@
-<html>
-  <head>
-    <meta charset='utf-8'>
-    <title><%= data.name || data.message %></title>
-    <style><%- include('style.css') %></style>
-  </head>
-  <body>
-    <div id="wrapper">
-      <h1><%= data.name %></h1>
-      <h2><em><%= data.statusCode %></em> <%= data.message %></h2>
-      <%
-        // display all the non-standard properties
-        var standardProps = ['name', 'statusCode', 'message', 'stack'];
-        for (var prop in data) {
-          if (standardProps.indexOf(prop) == -1 && data[prop]) { %>
-            <div><b><%= prop %></b>: <%= data[prop] %></div>
-          <% }
-        }
-        if (data.stack) { %>
-          <pre id="stacktrace"><%- data.stack %></pre>
-        <% }
-      %>
-    </div>
-  </body>
-</html>
diff --git a/views/default-error.hbs b/views/default-error.hbs
new file mode 100644
index 00000000..b7956384
--- /dev/null
+++ b/views/default-error.hbs
@@ -0,0 +1,25 @@
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>{{ data.name }}{{#unless data.name}}{{ data.message }}{{/unless}}</title>
+    <style>
+      {{partial 'style'}}
+    </style>
+  </head>
+  <body>
+    <div id="wrapper">
+      <h1>{{ data.name }}</h1>
+      <h2>
+        <em>{{ data.statusCode }}</em> {{ data.message }}
+      </h2>
+      {{#each data}}
+        {{#standardProps @key}}
+          <div><b>{{@key}}</b>: {{this}}</div>
+        {{/standardProps}}
+      {{/each}}
+      {{#if data.stack}}
+        <pre id="stacktrace">{{{data.stack}}}</pre>
+      {{/if}}
+    </div>
+  </body>
+</html>
diff --git a/views/style.css b/views/style.hbs
similarity index 100%
rename from views/style.css
rename to views/style.hbs