CVE-2020-7677: Insufficient Information (NVD-CWE-noinfo)
value: This affects the package thenify before 3.3.1. The name argument provided to the package can be controlled by users without any sanitization, and this is provided to the eval function without any sanitization.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 9.8
Published: 2022-07-25T14:15Z
Repository: https://github.com/thenables/thenify
Language: JavaScript
Files changed: index.js, test/test.js
Commit date: 2020-06-18 01:26:11 +0800
Commit message: fix: remove eval (#30)


Diff: diff --git a/index.js b/index.js
index 161d32e..d633174 100644
--- a/index.js
+++ b/index.js
@@ -5,37 +5,38 @@ var assert = require('assert')
 module.exports = thenify
 
 /**
  * Turn async functions into promises
  *
- * @param {Function} $$__fn__$$
+ * @param {Function} fn
  * @return {Function}
  * @api public
  */
 
-function thenify($$__fn__$$, options) {
-  assert(typeof $$__fn__$$ === 'function')
-  return eval(createWrapper($$__fn__$$.name, options))
+function thenify(fn, options) {
+  assert(typeof fn === 'function')
+  return createWrapper(fn, options)
 }
 
 /**
  * Turn async functions into promises and backward compatible with callback
  *
- * @param {Function} $$__fn__$$
+ * @param {Function} fn
  * @return {Function}
  * @api public
  */
 
-thenify.withCallback = function ($$__fn__$$, options) {
-  assert(typeof $$__fn__$$ === 'function')
+thenify.withCallback = function (fn, options) {
+  assert(typeof fn === 'function')
   options = options || {}
   options.withCallback = true
-  if (options.multiArgs === undefined) options.multiArgs = true
-  return eval(createWrapper($$__fn__$$.name, options))
+  return createWrapper(fn, options)
 }
 
 function createCallback(resolve, reject, multiArgs) {
+  // default to true
+  if (multiArgs === undefined) multiArgs = true
   return function(err, value) {
     if (err) return reject(err)
     var length = arguments.length
 
     if (length <= 2 || !multiArgs) return resolve(value)
@@ -50,31 +51,27 @@ function createCallback(resolve, reject, multiArgs) {
     for (var i = 1; i < length; ++i) values[i - 1] = arguments[i]
     resolve(values)
   }
 }
 
-function createWrapper(name, options) {
-  name = (name || '').replace(/\s|bound(?!$)/g, '')
+function createWrapper(fn, options) {
   options = options || {}
-  // default to true
-  var multiArgs = options.multiArgs !== undefined ? options.multiArgs : true
-  multiArgs = 'var multiArgs = ' + JSON.stringify(multiArgs) + '\n'
-
-  var withCallback = options.withCallback ?
-    'var lastType = typeof arguments[len - 1]\n'
-    + 'if (lastType === "function") return $$__fn__$$.apply(self, arguments)\n'
-   : ''
-
-  return '(function ' + name + '() {\n'
-    + 'var self = this\n'
-    + 'var len = arguments.length\n'
-    + multiArgs
-    + withCallback
-    + 'var args = new Array(len + 1)\n'
-    + 'for (var i = 0; i < len; ++i) args[i] = arguments[i]\n'
-    + 'var lastIndex = i\n'
-    + 'return new Promise(function (resolve, reject) {\n'
-      + 'args[lastIndex] = createCallback(resolve, reject, multiArgs)\n'
-      + '$$__fn__$$.apply(self, args)\n'
-    + '})\n'
-  + '})'
+  var name = fn.name;
+  name = (name || '').replace(/\s|bound(?!$)/g, '')
+  var newFn = function () {
+    var self = this
+    var len = arguments.length
+    if (options.withCallback) {
+      var lastType = typeof arguments[len - 1]
+      if (lastType === 'function') return fn.apply(self, arguments)
+    }
+    var args = new Array(len + 1)
+    for (var i = 0; i < len; ++i) args[i] = arguments[i]
+    var lastIndex = i
+    return new Promise(function (resolve, reject) {
+      args[lastIndex] = createCallback(resolve, reject, options.multiArgs)
+      fn.apply(self, args)
+    })
+  }
+  Object.defineProperty(newFn, 'name', { value: name })
+  return newFn
 }
diff --git a/test/test.js b/test/test.js
index 930816b..38d4a84 100644
--- a/test/test.js
+++ b/test/test.js
@@ -67,5 +67,29 @@ it('fn(..args, callback())', function () {
 
   return thenify(fn)(1, 2, 3).then(function (values) {
     assert.deepEqual(values, [1, 2, 3])
   })
 })
+
+it('unicode function name', function () {
+  function 你好$hello_123(a, b, c, cb) {
+    cb(null, a, b, c)
+  }
+  var wrapper = thenify(你好$hello_123)
+  assert.equal(wrapper.name, '你好$hello_123')
+  wrapper(1, 2, 3).then(function (values) {
+    assert.deepEqual(values, [1, 2, 3])
+  })
+})
+
+it('invalid function name', function () {
+  function fn(a, b, c, cb) {
+    cb(null, a, b, c)
+  }
+
+  Object.defineProperty(fn, 'name', { value: 'fake(){a.b;})();(function(){//' })
+  var wrapper = thenify(fn)
+  assert.equal(wrapper.name, fn.name)
+  wrapper(1, 2, 3).then(function (values) {
+    assert.deepEqual(values, [1, 2, 3])
+  })
+})

Vulnerable code:  * @param {Function} $$__fn__$$
function thenify($$__fn__$$, options) {
  assert(typeof $$__fn__$$ === 'function')
  return eval(createWrapper($$__fn__$$.name, options))
 * @param {Function} $$__fn__$$
thenify.withCallback = function ($$__fn__$$, options) {
  assert(typeof $$__fn__$$ === 'function')
  if (options.multiArgs === undefined) options.multiArgs = true
  return eval(createWrapper($$__fn__$$.name, options))
function createWrapper(name, options) {
  name = (name || '').replace(/\s|bound(?!$)/g, '')
  // default to true
  var multiArgs = options.multiArgs !== undefined ? options.multiArgs : true
  multiArgs = 'var multiArgs = ' + JSON.stringify(multiArgs) + '\n'

  var withCallback = options.withCallback ?
    'var lastType = typeof arguments[len - 1]\n'
    + 'if (lastType === "function") return $$__fn__$$.apply(self, arguments)\n'
   : ''

  return '(function ' + name + '() {\n'
    + 'var self = this\n'
    + 'var len = arguments.length\n'
    + multiArgs
    + withCallback
    + 'var args = new Array(len + 1)\n'
    + 'for (var i = 0; i < len; ++i) args[i] = arguments[i]\n'
    + 'var lastIndex = i\n'
    + 'return new Promise(function (resolve, reject) {\n'
      + 'args[lastIndex] = createCallback(resolve, reject, multiArgs)\n'
      + '$$__fn__$$.apply(self, args)\n'
    + '})\n'
  + '})'
Fixed code:  * @param {Function} fn
function thenify(fn, options) {
  assert(typeof fn === 'function')
  return createWrapper(fn, options)
 * @param {Function} fn
thenify.withCallback = function (fn, options) {
  assert(typeof fn === 'function')
  return createWrapper(fn, options)
  // default to true
  if (multiArgs === undefined) multiArgs = true
function createWrapper(fn, options) {
  var name = fn.name;
  name = (name || '').replace(/\s|bound(?!$)/g, '')
  var newFn = function () {
    var self = this
    var len = arguments.length
    if (options.withCallback) {
      var lastType = typeof arguments[len - 1]
      if (lastType === 'function') return fn.apply(self, arguments)
    }
    var args = new Array(len + 1)
    for (var i = 0; i < len; ++i) args[i] = arguments[i]
    var lastIndex = i
    return new Promise(function (resolve, reject) {
      args[lastIndex] = createCallback(resolve, reject, options.multiArgs)
      fn.apply(self, args)
    })
  }
  Object.defineProperty(newFn, 'name', { value: name })
  return newFn

it('unicode function name', function () {
  function 你好$hello_123(a, b, c, cb) {
    cb(null, a, b, c)
  }
  var wrapper = thenify(你好$hello_123)
  assert.equal(wrapper.name, '你好$hello_123')
  wrapper(1, 2, 3).then(function (values) {
    assert.deepEqual(values, [1, 2, 3])
  })
})

it('invalid function name', function () {
  function fn(a, b, c, cb) {
    cb(null, a, b, c)
  }

  Object.defineProperty(fn, 'name', { value: 'fake(){a.b;})();(function(){//' })
  var wrapper = thenify(fn)
  assert.equal(wrapper.name, fn.name)
  wrapper(1, 2, 3).then(function (values) {
    assert.deepEqual(values, [1, 2, 3])
  })
})