[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1010388: buster-pu: package node-ejs/2.5.7-1+deb10u1



Package: release.debian.org
Severity: normal
Tags: buster
User: release.debian.org@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 <yadd@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.commits@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 <yadd@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

Reply to: