From 37fefdc186a7ef62a263344b79677438422b2646 Mon Sep 17 00:00:00 2001 From: KalleV 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 --- 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 8ba8feb6..de1769b2 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", @@ -1557,6 +1515,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", @@ -1573,6 +1551,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" } @@ -1842,23 +1821,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", @@ -2106,6 +2068,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", @@ -2230,6 +2200,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", @@ -2791,6 +2766,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", @@ -2925,6 +2908,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" }, @@ -3005,6 +2989,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", @@ -3055,6 +3051,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 5c55e923..9bef7a42 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( - /Error<img onerror=alert\(1\) src=a><\/title>/, + // eslint-disable-next-line max-len + /<title>Error<img onerror=alert\(1\) src=a><\/title>/, ); expect(body).to.match( - /with id <img onerror=alert\(1\) src=a> found for Model/, + // eslint-disable-next-line max-len + /with id <img onerror=alert\(1\) src=a> 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<img onerror=alert\(1\) src=a>/, + // eslint-disable-next-line max-len + /500(.*?)a test error message<img onerror=alert\(1\) src=a>/, 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 %> - - - -
-

<%= data.name %>

-

<%= data.statusCode %> <%= data.message %>

- <% - // display all the non-standard properties - var standardProps = ['name', 'statusCode', 'message', 'stack']; - for (var prop in data) { - if (standardProps.indexOf(prop) == -1 && data[prop]) { %> -
<%= prop %>: <%= data[prop] %>
- <% } - } - if (data.stack) { %> -
<%- data.stack %>
- <% } - %> -
- - 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 @@ + + + + {{ data.name }}{{#unless data.name}}{{ data.message }}{{/unless}} + + + +
+

{{ data.name }}

+

+ {{ data.statusCode }} {{ data.message }} +

+ {{#each data}} + {{#standardProps @key}} +
{{@key}}: {{this}}
+ {{/standardProps}} + {{/each}} + {{#if data.stack}} +
{{{data.stack}}}
+ {{/if}} +
+ + diff --git a/views/style.css b/views/style.hbs similarity index 100% rename from views/style.css rename to views/style.hbs