CVE-2020-15095: Insertion of Sensitive Information into Log File (CWE-532)
value: Versions of the npm CLI prior to 6.14.6 are vulnerable to an information exposure vulnerability through log files. The CLI supports URLs like "<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>". The password value is not redacted and is printed to stdout and also to any generated log files.
CWE description: Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
CVSS v3 base score: 4.4
Published: 2020-07-07T19:15Z
Repository: https://github.com/npm/cli
Language: JavaScript
Files changed: bin/npm-cli.js, lib/fetch-package-metadata.js, lib/utils/error-handler.js, lib/utils/error-message.js, lib/utils/replace-info.js
Commit date: 2020-06-30 13:38:27 -0500
Commit message: chore: remove auth info from logs

Diff: diff --git a/bin/npm-cli.js b/bin/npm-cli.js
index 93eddc7a3..c0d9be004 100755
--- a/bin/npm-cli.js
+++ b/bin/npm-cli.js
@@ -26,10 +26,11 @@
   unsupported.checkForUnsupportedNode()
 
   var npm = require('../lib/npm.js')
   var npmconf = require('../lib/config/core.js')
   var errorHandler = require('../lib/utils/error-handler.js')
+  var replaceInfo = require('../lib/utils/replace-info.js')
 
   var configDefs = npmconf.defs
   var shorthands = configDefs.shorthands
   var types = configDefs.types
   var nopt = require('nopt')
@@ -38,11 +39,12 @@
   // run in global mode.
   if (process.argv[1][process.argv[1].length - 1] === 'g') {
     process.argv.splice(1, 1, 'npm', '-g')
   }
 
-  log.verbose('cli', process.argv)
+  var args = replaceInfo(process.argv)
+  log.verbose('cli', args)
 
   var conf = nopt(types, shorthands)
   npm.argv = conf.argv.remain
   if (npm.deref(npm.argv[0])) npm.command = npm.argv.shift()
   else conf.usage = true
diff --git a/lib/fetch-package-metadata.js b/lib/fetch-package-metadata.js
index c4f46f513..ebb378647 100644
--- a/lib/fetch-package-metadata.js
+++ b/lib/fetch-package-metadata.js
@@ -1,27 +1,30 @@
 'use strict'
 
 const deprCheck = require('./utils/depr-check')
 const path = require('path')
 const log = require('npmlog')
+const pacote = require('pacote')
 const readPackageTree = require('read-package-tree')
 const rimraf = require('rimraf')
 const validate = require('aproba')
 const npa = require('npm-package-arg')
 const npm = require('./npm')
 let npmConfig
 const npmlog = require('npmlog')
 const limit = require('call-limit')
-const tempFilename = require('./utils/temp-filename')
-const pacote = require('pacote')
+const tempFilename = require('./utils/temp-filename.js')
+const replaceInfo = require('./utils/replace-info.js')
 const isWindows = require('./utils/is-windows.js')
 
 function andLogAndFinish (spec, tracker, done) {
   validate('SOF|SZF|OOF|OZF', [spec, tracker, done])
   return (er, pkg) => {
     if (er) {
-      log.silly('fetchPackageMetaData', 'error for ' + String(spec), er.message)
+      er.message = replaceInfo(er.message)
+      var spc = replaceInfo(String(spec))
+      log.silly('fetchPackageMetaData', 'error for ' + spc, er.message)
       if (tracker) tracker.finish()
     }
     return done(er, pkg)
   }
 }
diff --git a/lib/utils/error-handler.js b/lib/utils/error-handler.js
index 3e6f176ef..902ad7af9 100644
--- a/lib/utils/error-handler.js
+++ b/lib/utils/error-handler.js
@@ -10,10 +10,11 @@ var path = require('path')
 var wroteLogFile = false
 var exitCode = 0
 var rollbacks = npm.rollbacks
 var chain = require('slide').chain
 var errorMessage = require('./error-message.js')
+var replaceInfo = require('./replace-info.js')
 var stopMetrics = require('./metrics.js').stop
 
 const cacheFile = require('./cache-file.js')
 
 var logFileName
@@ -173,18 +174,20 @@ function errorHandler (er) {
     'statusCode',
     'pkgid'
   ].forEach(function (k) {
     var v = er[k]
     if (!v) return
+    v = replaceInfo(v)
     log.verbose(k, v)
   })
 
   log.verbose('cwd', process.cwd())
 
   var os = require('os')
+  var args = replaceInfo(process.argv)
   log.verbose('', os.type() + ' ' + os.release())
-  log.verbose('argv', process.argv.map(JSON.stringify).join(' '))
+  log.verbose('argv', args.map(JSON.stringify).join(' '))
   log.verbose('node', process.version)
   log.verbose('npm ', 'v' + npm.version)
 
   ;[
     'code',
diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js
index 3faa78f30..78ccdeeb5 100644
--- a/lib/utils/error-message.js
+++ b/lib/utils/error-message.js
@@ -1,16 +1,21 @@
 'use strict'
 var npm = require('../npm.js')
 var util = require('util')
 var nameValidator = require('validate-npm-package-name')
 var npmlog = require('npmlog')
+var replaceInfo = require('./replace-info.js')
 
 module.exports = errorMessage
 
 function errorMessage (er) {
   var short = []
   var detail = []
+
+  er.message = replaceInfo(er.message)
+  er.stack = replaceInfo(er.stack)
+
   switch (er.code) {
     case 'ENOAUDIT':
       short.push(['audit', er.message])
       break
     case 'EAUDITNOPJSON':
diff --git a/lib/utils/replace-info.js b/lib/utils/replace-info.js
new file mode 100644
index 000000000..a613a3755
--- /dev/null
+++ b/lib/utils/replace-info.js
@@ -0,0 +1,22 @@
+const URL = require('url')
+
+// replaces auth info in an array
+//  of arguments or in a strings
+function replaceInfo (arg) {
+  const isArray = Array.isArray(arg)
+  const isString = typeof arg === 'string'
+
+  if (!isArray && !isString) return arg
+
+  const args = isString ? arg.split(' ') : arg
+  const info = args.map(arg => {
+    try {
+      const url = new URL(arg)
+      return url.password === '' ? arg : arg.replace(url.password, '***')
+    } catch (e) { return arg }
+  })
+
+  return isString ? info.join(' ') : info
+}
+
+module.exports = replaceInfo

Vulnerable code:   log.verbose('cli', process.argv)
const tempFilename = require('./utils/temp-filename')
const pacote = require('pacote')
      log.silly('fetchPackageMetaData', 'error for ' + String(spec), er.message)
  log.verbose('argv', process.argv.map(JSON.stringify).join(' '))
Fixed code:   var replaceInfo = require('../lib/utils/replace-info.js')
  var args = replaceInfo(process.argv)
  log.verbose('cli', args)
const pacote = require('pacote')
const tempFilename = require('./utils/temp-filename.js')
const replaceInfo = require('./utils/replace-info.js')
      er.message = replaceInfo(er.message)
      var spc = replaceInfo(String(spec))
      log.silly('fetchPackageMetaData', 'error for ' + spc, er.message)
var replaceInfo = require('./replace-info.js')
    v = replaceInfo(v)
  var args = replaceInfo(process.argv)
  log.verbose('argv', args.map(JSON.stringify).join(' '))
var replaceInfo = require('./replace-info.js')

  er.message = replaceInfo(er.message)
  er.stack = replaceInfo(er.stack)

const URL = require('url')

// replaces auth info in an array
//  of arguments or in a strings
function replaceInfo (arg) {
  const isArray = Array.isArray(arg)
  const isString = typeof arg === 'string'

  if (!isArray && !isString) return arg

  const args = isString ? arg.split(' ') : arg
  const info = args.map(arg => {
    try {
      const url = new URL(arg)
      return url.password === '' ? arg : arg.replace(url.password, '***')
    } catch (e) { return arg }
  })

  return isString ? info.join(' ') : info
}

module.exports = replaceInfo