Skip to content

Commit f1c7344

Browse files
committed
Redact protected config values in argv logs
1 parent b97edc0 commit f1c7344

4 files changed

Lines changed: 92 additions & 40 deletions

File tree

lib/commands/config.js

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,7 @@ const { defaults, definitions, nerfDarts, proxyEnv } = require('@npmcli/config/l
88
const { log, output, input } = require('proc-log')
99
const BaseCommand = require('../base-cmd.js')
1010
const { redact } = require('@npmcli/redact')
11-
12-
// These are the config values to swap with "protected".
13-
// It does not catch every single sensitive thing a user may put in the npmrc file but it gets the common ones.
14-
// This is distinct from nerfDarts because that is used to validate valid configs during "npm config set", and folks may have old invalid entries lying around in a config file that we still want to protect when running "npm config list"
15-
// This is a more general list of values to consider protected.
16-
// You cannot "npm config get" them, and they will not display during "npm config list"
17-
const protected = [
18-
'auth',
19-
'authToken',
20-
'certfile',
21-
'email',
22-
'keyfile',
23-
'password',
24-
'username',
25-
]
11+
const { isProtected } = require('../utils/protected-config.js')
2612

2713
// take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into { key: value, k2: v2, k3: v3 }
2814
const keyValues = args => {
@@ -38,29 +24,6 @@ const keyValues = args => {
3824
return kv
3925
}
4026

41-
const isProtected = (k) => {
42-
// _password
43-
if (k.startsWith('_')) {
44-
return true
45-
}
46-
if (protected.includes(k)) {
47-
return true
48-
}
49-
// //localhost:8080/:_password
50-
if (k.startsWith('//')) {
51-
if (k.includes(':_')) {
52-
return true
53-
}
54-
// //registry:_authToken or //registry:authToken
55-
for (const p of protected) {
56-
if (k.endsWith(`:${p}`) || k.endsWith(`:_${p}`)) {
57-
return true
58-
}
59-
}
60-
}
61-
return false
62-
}
63-
6427
// Private fields are either protected or they can redacted info
6528
const isPrivate = (k, v) => isProtected(k) || redact(v) !== v
6629

lib/npm.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,35 @@ const { redactLog: replaceInfo } = require('@npmcli/redact')
1111
const pkg = require('../package.json')
1212
const { deref } = require('./utils/cmd-list.js')
1313
const { jsonError, outputError } = require('./utils/output-error.js')
14+
const { isProtected } = require('./utils/protected-config.js')
15+
16+
const REDACTED = '***'
17+
18+
const cleanArgv = (argv) => {
19+
const cleaned = replaceInfo(argv)
20+
for (let i = 0; i < cleaned.length; i++) {
21+
const arg = cleaned[i]
22+
if (typeof arg !== 'string' || !arg.startsWith('--')) {
23+
continue
24+
}
25+
26+
const raw = arg.slice(2)
27+
const equalsIndex = raw.indexOf('=')
28+
const key = equalsIndex === -1 ? raw : raw.slice(0, equalsIndex)
29+
if (!isProtected(key)) {
30+
continue
31+
}
32+
33+
if (equalsIndex === -1) {
34+
if (i + 1 < cleaned.length) {
35+
cleaned[i + 1] = REDACTED
36+
}
37+
} else {
38+
cleaned[i] = `${arg.slice(0, equalsIndex + 3)}${REDACTED}`
39+
}
40+
}
41+
return cleaned
42+
}
1443

1544
class Npm {
1645
static get version () {
@@ -142,8 +171,7 @@ class Npm {
142171
process.title = this.#title
143172
// The cooked argv is also logged separately for debugging purposes.
144173
// It is cleaned as a best effort by replacing known secrets like basic auth password and strings that look like npm tokens.
145-
// XXX: for this to be safer the config should create a sanitized version of the argv as it has the full context of what each option contains.
146-
this.#argvClean = replaceInfo(cooked)
174+
this.#argvClean = cleanArgv(cooked)
147175
log.verbose('title', this.title)
148176
log.verbose('argv', this.#argvClean.map(JSON.stringify).join(' '))
149177
})

lib/utils/protected-config.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// These are config values whose contents should not be displayed.
2+
// This is intentionally broader than nerfDarts because users may have old or
3+
// invalid auth entries that still need to be protected when npm reports config.
4+
const protected = [
5+
'auth',
6+
'authToken',
7+
'certfile',
8+
'email',
9+
'keyfile',
10+
'password',
11+
'username',
12+
]
13+
14+
const isProtected = (key) => {
15+
// _password
16+
if (key.startsWith('_')) {
17+
return true
18+
}
19+
if (protected.includes(key)) {
20+
return true
21+
}
22+
// //localhost:8080/:_password
23+
if (key.startsWith('//')) {
24+
if (key.includes(':_')) {
25+
return true
26+
}
27+
// //registry:authToken or //registry:_authToken
28+
for (const protectedKey of protected) {
29+
if (key.endsWith(`:${protectedKey}`) || key.endsWith(`:_${protectedKey}`)) {
30+
return true
31+
}
32+
}
33+
}
34+
return false
35+
}
36+
37+
module.exports = {
38+
isProtected,
39+
}

test/lib/cli/entry.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,28 @@ t.test('logged argv is sanitized with equals', async t => {
105105
t.match(logs.verbose.byTitle('argv'), ['argv "version" "--registry" "https://u:***@npmjs.org"'])
106106
})
107107

108+
t.test('logged argv redacts protected config values', async t => {
109+
const { logs, cli } = await cliMock(t, {
110+
globals: {
111+
'process.argv': [
112+
'node',
113+
'npm',
114+
'version',
115+
'--//registry.npmjs.org/:_authToken=plain-secret',
116+
'--_password',
117+
'hunter2',
118+
],
119+
},
120+
})
121+
await cli(process)
122+
123+
const argv = logs.verbose.byTitle('argv')[0]
124+
t.notMatch(argv, 'plain-secret')
125+
t.notMatch(argv, 'hunter2')
126+
t.match(argv, '"--//registry.npmjs.org/:_authToken" "***"')
127+
t.match(argv, '"--_password" "***"')
128+
})
129+
108130
t.test('print usage if no params provided', async t => {
109131
const { cli, outputs, exitHandlerCalled, exitHandlerNpm } = await cliMock(t, {
110132
globals: {

0 commit comments

Comments
 (0)