--- Begin Message ---
- To: Debian Bug Tracking System <submit@bugs.debian.org>
- Subject: buster-pu: package node-ejs/2.5.7-1+deb10u1
- From: Yadd <yadd@debian.org>
- Date: Sat, 30 Apr 2022 10:23:37 +0200
- Message-id: <165130701758.1037077.12977832390451624929.reportbug@debian007.xnr.fr>
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
--- End Message ---