Package: release.debian.org Severity: normal Tags: buster User: release.debian....@packages.debian.org Usertags: pu
[ Reason ] node-ejs is vulnerable to server-side template injection (CVE-2022-29078, #1010359) and probably to prototype pollution. [ Impact ] Medium security issue [ Tests ] New test added, confirms that issue is fixed (sadly locally only, test isn't launched in buster). Patch is the same than for Bullseye (except test) and test passed in it. [ Risks ] Low risk, code is trivial [ Checklist ] [X] *all* changes are documented in the d/changelog [X] I reviewed all changes and I approve them [X] attach debdiff against the package in (old)stable [X] the issue is verified as fixed in unstable [ Changes ] * Replace {} by `new Object` * check localsName value Cheers, Yadd
diff --git a/debian/changelog b/debian/changelog index 3a9ce9c..68d1536 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +node-ejs (2.5.7-1+deb10u1) buster; urgency=medium + + * Team upload + * Sanitize options and new objects (Closes: #1010359, CVE-2022-29078) + + -- Yadd <y...@debian.org> Sat, 30 Apr 2022 10:18:39 +0200 + node-ejs (2.5.7-1) unstable; urgency=medium * Team upload diff --git a/debian/patches/CVE-2022-29078.patch b/debian/patches/CVE-2022-29078.patch new file mode 100644 index 0000000..ec85061 --- /dev/null +++ b/debian/patches/CVE-2022-29078.patch @@ -0,0 +1,174 @@ +Description: sanitize localsName option and fix prototype pollution + This patch fixes CVE-2022-29078 but I also apply prototype pollution fixes, + even if there are no CVE associated with it +Author: Nicolas Dumazet <nicdumz.comm...@gmail.com> +Origin: upstream, https://github.com/mde/ejs/commit/15ee6985 +Bug: https://eslam.io/posts/ejs-server-side-template-injection-rce/ +Bug-Debian: https://bugs.debian.org/1010359 +Forwarded: not-needed +Reviewed-By: Yadd <y...@debian.org> +Last-Update: 2022-04-30 + +--- a/lib/ejs.js ++++ b/lib/ejs.js +@@ -61,6 +61,7 @@ + // so we make an exception for `renderFile` + var _OPTS_EXPRESS = _OPTS.concat('cache'); + var _BOM = /^\uFEFF/; ++var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + + /** + * EJS template function cache. This can be a LRU object from lru-cache NPM +@@ -254,7 +255,7 @@ + */ + + function includeFile(path, options) { +- var opts = utils.shallowCopy({}, options); ++ var opts = utils.shallowCopy(utils.createNullProtoObjWherePossible(), options); + opts.filename = getIncludePath(path, opts); + return handleCache(opts); + } +@@ -270,7 +271,7 @@ + */ + + function includeSource(path, options) { +- var opts = utils.shallowCopy({}, options); ++ var opts = utils.shallowCopy(utils.createNullProtoObjWherePossible(), options); + var includePath; + var template; + includePath = getIncludePath(path, opts); +@@ -372,8 +373,8 @@ + */ + + exports.render = function (template, d, o) { +- var data = d || {}; +- var opts = o || {}; ++ var data = d || utils.createNullProtoObjWherePossible(); ++ var opts = o || utils.createNullProtoObjWherePossible(); + + // No options object -- if there are optiony names + // in the data, copy them to options +@@ -431,7 +432,7 @@ + opts.filename = filename; + } + else { +- data = {}; ++ data = utils.createNullProtoObjWherePossible(); + } + + return tryHandleCache(opts, data, cb); +@@ -447,8 +448,8 @@ + }; + + function Template(text, opts) { +- opts = opts || {}; +- var options = {}; ++ opts = opts || utils.createNullProtoObjWherePossible(); ++ var options = utils.createNullProtoObjWherePossible(); + this.templateText = text; + this.mode = null; + this.truncate = false; +@@ -466,6 +467,9 @@ + options.cache = opts.cache || false; + options.rmWhitespace = opts.rmWhitespace; + options.root = opts.root; ++ if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) { ++ throw new Error('localsName is not a valid JS identifier.'); ++ } + options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME; + options.views = opts.views; + +@@ -571,13 +575,13 @@ + // Adds a local `include` function which allows full recursive include + var returnedFn = function (data) { + var include = function (path, includeData) { +- var d = utils.shallowCopy({}, data); ++ var d = utils.shallowCopy(utils.createNullProtoObjWherePossible(), data); + if (includeData) { + d = utils.shallowCopy(d, includeData); + } + return includeFile(path, opts)(d); + }; +- return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow]); ++ return fn.apply(opts.context, [data || utils.createNullProtoObjWherePossible(), escapeFn, include, rethrow]); + }; + returnedFn.dependencies = this.dependencies; + return returnedFn; +--- a/lib/utils.js ++++ b/lib/utils.js +@@ -114,8 +114,10 @@ + */ + exports.shallowCopy = function (to, from) { + from = from || {}; +- for (var p in from) { +- to[p] = from[p]; ++ if ((to !== null) && (to !== undefined)) { ++ for (var p in from) { ++ to[p] = from[p]; ++ } + } + return to; + }; +@@ -133,12 +135,16 @@ + * @private + */ + exports.shallowCopyFromList = function (to, from, list) { ++ list = list || []; ++ from = from || {}; ++ if ((to !== null) && (to !== undefined)) { + for (var i = 0; i < list.length; i++) { + var p = list[i]; + if (typeof from[p] != 'undefined') { + to[p] = from[p]; + } + } ++ } + return to; + }; + +@@ -162,3 +168,27 @@ + this._data = {}; + } + }; ++ ++/** ++ * Returns a null-prototype object in runtimes that support it ++ * ++ * @return {Object} Object, prototype will be set to null where possible ++ * @static ++ * @private ++ */ ++exports.createNullProtoObjWherePossible = (function () { ++ if (typeof Object.create == 'function') { ++ return function () { ++ return Object.create(null); ++ }; ++ } ++ if (!({__proto__: null} instanceof Object)) { ++ return function () { ++ return {__proto__: null}; ++ }; ++ } ++ // Not possible, just pass through ++ return function () { ++ return {}; ++ }; ++})(); +--- a/test/ejs.js ++++ b/test/ejs.js +@@ -1147,3 +1147,15 @@ + assert.strictEqual(ejs.name, 'ejs'); + }); + }); ++ ++suite('identifier validation', function () { ++ test('should reject invalid localsName', function () { ++ var locals = Object.create(null); ++ assert.throws(function() { ++ ejs.compile('<p>yay</p>', { ++ localsName: 'function(){console.log(1);return locals;}()' ++ }); ++ }, /localsName is not a valid JS identifier/ ++ ); ++ }) ++}); diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000..32e1773 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +CVE-2022-29078.patch