@mossbear/eval-core 0.1.0-alpha.106 → 0.1.0-alpha.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/redact-prose.d.ts +5 -41
- package/dist/redact-prose.d.ts.map +1 -1
- package/dist/redact-prose.js +9 -67
- package/dist/redact-prose.js.map +1 -1
- package/dist/redact-prose.test.js +2 -6
- package/dist/redact-prose.test.js.map +1 -1
- package/dist/redaction.d.ts +2 -10
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +2 -10
- package/dist/redaction.js.map +1 -1
- package/dist/sanitize-command.d.ts +0 -19
- package/dist/sanitize-command.d.ts.map +1 -1
- package/dist/sanitize-command.js +73 -1545
- package/dist/sanitize-command.js.map +1 -1
- package/dist/sanitize-command.test.js +44 -1309
- package/dist/sanitize-command.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,1313 +1,48 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { sanitizeCommand } from './sanitize-command.js';
|
|
3
|
-
describe('
|
|
4
|
-
it(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
expect(sanitizeCommand(
|
|
21
|
-
expect(sanitizeCommand(
|
|
22
|
-
});
|
|
23
|
-
it(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const redacted = sanitizeCommand(`curl -d ${argument}`);
|
|
47
|
-
expect(redacted).not.toContain('fixture');
|
|
48
|
-
expect(redacted).toContain('hello');
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
it('redacts JSON bodies with shell-only escapes and malformed JSON', () => {
|
|
52
|
-
for (const command of [
|
|
53
|
-
String.raw `curl -d "{\"password\":\"fixture\$password\"}"`,
|
|
54
|
-
String.raw `curl -d "{\"password\":\"fixture\!password\"}"`,
|
|
55
|
-
String.raw `curl -d "{\"password\":\"fixture\\!password\"}"`,
|
|
56
|
-
'curl -d "{\\"password\\":\\"fixture\\`password\\"}"',
|
|
57
|
-
String.raw `curl -d "{\"authorization\":\"Basic fixture\$credential\"}"`,
|
|
58
|
-
String.raw `curl -d '{"password":"fixture-password",}'`,
|
|
59
|
-
]) {
|
|
60
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
it('redacts token authorization schemes', () => {
|
|
64
|
-
const payload = ['fixture', 'credential'].join('-');
|
|
65
|
-
expect(sanitizeCommand(`curl -H "Authorization: token ${payload}"`)).not.toContain(payload);
|
|
66
|
-
});
|
|
67
|
-
it('redacts credentials in recursively JSON-encoded string values', () => {
|
|
68
|
-
let body = JSON.stringify({ password: 'fixture-password' });
|
|
69
|
-
for (let depth = 0; depth < 4; depth++) {
|
|
70
|
-
body = JSON.stringify({ config: body });
|
|
71
|
-
for (const argument of [`'${body}'`, JSON.stringify(body)]) {
|
|
72
|
-
expect(sanitizeCommand(`curl -d ${argument}`)).not.toContain('fixture');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
it('preserves quoted programs and brace expansion without credentials', () => {
|
|
77
|
-
for (const command of [
|
|
78
|
-
"awk '{print $1}' /var/log/syslog",
|
|
79
|
-
"jq '[.items[] | .name]' resp.json",
|
|
80
|
-
"echo '{a,b}'",
|
|
81
|
-
]) {
|
|
82
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
it('redacts complete shell words with concatenated quote sections', () => {
|
|
86
|
-
const body = JSON.stringify({
|
|
87
|
-
name: "O'Brien",
|
|
88
|
-
config: JSON.stringify({ password: 'fixture-password' }),
|
|
89
|
-
});
|
|
90
|
-
for (const escaped of ["'\\''", "'\"'\"'"]) {
|
|
91
|
-
const argument = `'${body.replaceAll("'", escaped)}'`;
|
|
92
|
-
expect(sanitizeCommand(`curl -d ${argument}`)).not.toContain('fixture');
|
|
93
|
-
const value = `'fixture${escaped}password'`;
|
|
94
|
-
expect(sanitizeCommand(`tool --password ${value}`)).toBe('tool --password [redacted]');
|
|
95
|
-
expect(sanitizeCommand(`PASSWORD=${value} tool`)).toBe('PASSWORD=[redacted] tool');
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
it('bounds scanning time for long malformed JSON-like arguments', () => {
|
|
99
|
-
const command = `jq '{${'x'.repeat(128_000)}: .y}' file.json`;
|
|
100
|
-
const start = performance.now();
|
|
101
|
-
expect(sanitizeCommand(command)).toContain('[... truncated,');
|
|
102
|
-
expect(performance.now() - start).toBeLessThan(2000);
|
|
103
|
-
});
|
|
104
|
-
it('preserves author and oauth metadata', () => {
|
|
105
|
-
for (const command of [
|
|
106
|
-
'git commit --author="Jane Doe <jane@example.com>"',
|
|
107
|
-
`curl -d '${JSON.stringify({ author: 'Jane Doe', oauth_provider: 'example' })}'`,
|
|
108
|
-
'oauth_provider=example tool',
|
|
109
|
-
]) {
|
|
110
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
it('consumes shell-escaped whitespace in flag and assignment credentials', () => {
|
|
114
|
-
for (const command of [
|
|
115
|
-
String.raw `tool --password fixture\ secret --verbose`,
|
|
116
|
-
String.raw `PASSWORD=fixture\ secret tool`,
|
|
117
|
-
]) {
|
|
118
|
-
const redacted = sanitizeCommand(command);
|
|
119
|
-
expect(redacted).not.toContain('fixture');
|
|
120
|
-
expect(redacted).not.toContain(' secret');
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
it('leaves short benign commands untouched', () => {
|
|
124
|
-
expect(sanitizeCommand('bun test')).toBe('bun test');
|
|
125
|
-
expect(sanitizeCommand('git commit -m "fix: thing"')).toBe('git commit -m "fix: thing"');
|
|
126
|
-
});
|
|
127
|
-
it('redacts our own CLI token, including the space-separated flag form', () => {
|
|
128
|
-
// The documented setup command puts a live dashboard credential on the
|
|
129
|
-
// command line, where PostToolUse logs it and the next sync would upload
|
|
130
|
-
// it in an action summary. The `key=value` rule never sees a
|
|
131
|
-
// space-separated flag, so the token prefix has to be matched directly.
|
|
132
|
-
expect(sanitizeCommand('mossbear sync --token mossbear_tPtcsSvXyRxygMSkBjjDpBKz')).toBe('mossbear sync --token [redacted]');
|
|
133
|
-
expect(sanitizeCommand('mossbear init --token=mossbear_abcdefgh12345678')).toBe('mossbear init --token=[redacted]');
|
|
134
|
-
});
|
|
135
|
-
it('still redacts a token issued under the pre-rename prefix', () => {
|
|
136
|
-
// Those keys are deleted by migration 0056, but a CLI can be older or newer
|
|
137
|
-
// than the dashboard it syncs to; while one is still live, dropping the rule
|
|
138
|
-
// would upload it in plaintext.
|
|
139
|
-
expect(sanitizeCommand('mossbear sync --token auden_tPtcsSvXyRxygMSkBjjDpBKz')).toBe('mossbear sync --token [redacted]');
|
|
140
|
-
});
|
|
141
|
-
it('does not redact ordinary words that merely start with the prefix', () => {
|
|
142
|
-
expect(sanitizeCommand('cd mossbear_repo && ls')).toBe('cd mossbear_repo && ls');
|
|
143
|
-
expect(sanitizeCommand('cd auden_repo && ls')).toBe('cd auden_repo && ls');
|
|
144
|
-
});
|
|
145
|
-
it('redacts bearer tokens in Authorization headers', () => {
|
|
146
|
-
const out = sanitizeCommand('curl -H "Authorization: Bearer sk-abc123def456ghi789" https://api.example.com');
|
|
147
|
-
expect(out).not.toContain('sk-abc123def456ghi789');
|
|
148
|
-
expect(out).toContain('[redacted]');
|
|
149
|
-
expect(out).toContain('https://api.example.com');
|
|
150
|
-
});
|
|
151
|
-
it('redacts credentials in connection-string URLs', () => {
|
|
152
|
-
const out = sanitizeCommand('psql postgres://mossbear:s3cretpw@db.host:5432/mossbear');
|
|
153
|
-
expect(out).not.toContain('s3cretpw');
|
|
154
|
-
expect(out).toContain('postgres://mossbear:[redacted]@db.host:5432/mossbear');
|
|
155
|
-
});
|
|
156
|
-
it('redacts sensitive key=value assignments', () => {
|
|
157
|
-
const out = sanitizeCommand('OPENAI_API_KEY=sk-live-abcdef123456 bun run eval');
|
|
158
|
-
expect(out).not.toContain('abcdef123456');
|
|
159
|
-
expect(out).toContain('bun run eval');
|
|
160
|
-
});
|
|
161
|
-
it('redacts quoted secret values that contain spaces', () => {
|
|
162
|
-
const out = sanitizeCommand('PASSWORD="correct horse battery staple" deploy');
|
|
163
|
-
expect(out).not.toContain('correct horse battery staple');
|
|
164
|
-
expect(out).toContain('PASSWORD=[redacted]');
|
|
165
|
-
expect(out).toContain('deploy');
|
|
166
|
-
const single = sanitizeCommand("DB_PASSWORD='a b c d' run");
|
|
167
|
-
expect(single).not.toContain('a b c d');
|
|
168
|
-
expect(single).toContain('run');
|
|
169
|
-
});
|
|
170
|
-
it('strips heredoc bodies, keeping only the delimiters', () => {
|
|
171
|
-
const out = sanitizeCommand("cat > src/config.ts <<EOF\nexport const KEY = 'sk-verysecret12345'\nmore lines\nEOF");
|
|
172
|
-
expect(out).not.toContain('sk-verysecret12345');
|
|
173
|
-
expect(out).not.toContain('more lines');
|
|
174
|
-
expect(out).toContain('[redacted]');
|
|
175
|
-
expect(out).toContain('EOF');
|
|
176
|
-
});
|
|
177
|
-
it('redacts well-known token prefixes standing alone', () => {
|
|
178
|
-
for (const secret of [
|
|
179
|
-
'ghp_16C7e42F292c6912E7710c838347Ae178B4a', // oss-boundary-ignore — fake token shape
|
|
180
|
-
'github_pat_11ABCDEFG_abcdefghij', // oss-boundary-ignore — fake token shape
|
|
181
|
-
'xoxb-1234567890-abcdefghijk', // oss-boundary-ignore — fake token shape
|
|
182
|
-
'AKIAIOSFODNN7EXAMPLE', // oss-boundary-ignore — fake token shape
|
|
183
|
-
]) {
|
|
184
|
-
const out = sanitizeCommand(`deploy --with ${secret}`);
|
|
185
|
-
expect(out).not.toContain(secret);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
it('redacts private key blocks even without an END marker', () => {
|
|
189
|
-
const out = sanitizeCommand('cat > key.pem <<EOF\n-----BEGIN RSA PRIVATE KEY-----\nMIIEow\nEOF' // oss-boundary-ignore — synthetic fixture, not a real key
|
|
190
|
-
);
|
|
191
|
-
expect(out).not.toContain('MIIEow');
|
|
192
|
-
});
|
|
193
|
-
it('truncates long commands and notes omitted length', () => {
|
|
194
|
-
const long = `echo ${'x'.repeat(500)}`;
|
|
195
|
-
const out = sanitizeCommand(long);
|
|
196
|
-
expect(out.length).toBeLessThan(300);
|
|
197
|
-
expect(out).toContain('[... truncated,');
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
describe('command disclosure regressions', () => {
|
|
201
|
-
it('redacts cookie request headers', () => {
|
|
202
|
-
expect(sanitizeCommand(`curl -H 'Cookie: session=fixture-cookie; csrf=fixture-csrf' https://example.test`)).not.toContain('fixture-cookie');
|
|
203
|
-
expect(sanitizeCommand(`curl -b 'session=fixture-cookie' https://example.test`)).not.toContain('fixture-cookie');
|
|
204
|
-
});
|
|
205
|
-
it('redacts HTTP username/password arguments in short and long forms', () => {
|
|
206
|
-
for (const flag of ['-u ', '-u', '--user ', '--user=', '--proxy-user ']) {
|
|
207
|
-
expect(sanitizeCommand(`curl ${flag}'fixture-user:fixture-password' https://example.test`)).not.toContain('fixture-password');
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
it('redacts credentials after shell line continuations', () => {
|
|
211
|
-
for (const input of [
|
|
212
|
-
'tool --password \\\n fixture-continued',
|
|
213
|
-
'tool --password \\\r\n fixture-continued',
|
|
214
|
-
])
|
|
215
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-continued');
|
|
216
|
-
});
|
|
217
|
-
it('redacts the value of a credential flag that follows another credential flag', () => {
|
|
218
|
-
// The first flag's rule takes the second flag as its value; the second
|
|
219
|
-
// flag's own value must not escape because of it.
|
|
220
|
-
for (const input of [
|
|
221
|
-
'x --password \n --token reader:fixture-visible',
|
|
222
|
-
'x --password --token reader:fixture-visible',
|
|
223
|
-
'x --password\t--token\treader:fixture-visible',
|
|
224
|
-
'x --password\n--token\nreader:fixture-visible',
|
|
225
|
-
'x --password \\\n --token reader:fixture-visible',
|
|
226
|
-
'x --password "--token" reader:fixture-visible',
|
|
227
|
-
"x --password '--api-key' reader:fixture-visible",
|
|
228
|
-
'x --password --token --client-secret reader:fixture-visible',
|
|
229
|
-
'x `--password\n --token reader:fixture-visible`',
|
|
230
|
-
'x $(tool --password --token reader:fixture-visible)',
|
|
231
|
-
"sh -c 'tool --password --token reader:fixture-visible'",
|
|
232
|
-
'bash -c "tool --password --token reader:fixture-visible"',
|
|
233
|
-
'env tool --password --token reader:fixture-visible',
|
|
234
|
-
'sudo tool --password --token reader:fixture-visible',
|
|
235
|
-
'x --password --token abc--fixture-visible',
|
|
236
|
-
'x --password --token "a b--fixture-visible" --verbose',
|
|
237
|
-
"bash -c $'x --password --token \\\\\\n reader:fixture-visible'",
|
|
238
|
-
])
|
|
239
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
240
|
-
});
|
|
241
|
-
it('redacts a long credential chain without stalling the hook', () => {
|
|
242
|
-
// Each shape took minutes before the chain's key test stopped
|
|
243
|
-
// backtracking and a chain stopped at words another chain had left.
|
|
244
|
-
for (const input of [
|
|
245
|
-
`x --password --token ${'token'.repeat(4000)}`,
|
|
246
|
-
`x --password --${'token'.repeat(4000)}! y`,
|
|
247
|
-
`x ${Array.from({ length: 4000 }, () => '--token').join(' ')} fixture-visible`,
|
|
248
|
-
]) {
|
|
249
|
-
const output = sanitizeCommand(input);
|
|
250
|
-
expect(output).not.toContain('fixture-visible');
|
|
251
|
-
expect(output).not.toContain('tokentoken');
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
it('keeps redacting what a credential flag swallowed on its own', () => {
|
|
255
|
-
expect(sanitizeCommand('x --password\nfixture-secret')).toBe('x --password\n[redacted]');
|
|
256
|
-
expect(sanitizeCommand('x --password --token fixture-visible')).toBe('x --password [redacted] [redacted]');
|
|
257
|
-
expect(sanitizeCommand('x --password --token=fixture-visible')).toBe('x --password [redacted]');
|
|
258
|
-
// A swallowed word that is not a credential flag ends the chain there.
|
|
259
|
-
expect(sanitizeCommand('x --password --verbose fixture-kept')).toBe('x --password [redacted] fixture-kept');
|
|
260
|
-
expect(sanitizeCommand('x --password --token; echo fixture-kept')).toBe('x --password [redacted]; echo fixture-kept');
|
|
261
|
-
// Following the chain must not hide a flag a later rule reads: here the
|
|
262
|
-
// newline-crossing rule redacts --PASSWORD's value only while the flag
|
|
263
|
-
// itself is still in the text.
|
|
264
|
-
for (const input of [
|
|
265
|
-
'x $\'--credentials\' "--token" --PASSWORD \n fixture-visible',
|
|
266
|
-
'tool --auth-token\t\\\n\t--token\treader:fixture-visible',
|
|
267
|
-
'bash -c "x --password \'--token\' \\\\\n --api-key=fixture-visible"',
|
|
268
|
-
])
|
|
269
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
270
|
-
});
|
|
271
|
-
it('redacts a credential flag value after a tool password flag swallowed the flag', () => {
|
|
272
|
-
// Each tool flag takes the next word as its value; when that word is a
|
|
273
|
-
// credential flag, the flag's own value must not escape.
|
|
274
|
-
for (const [input, output] of [
|
|
275
|
-
[
|
|
276
|
-
'redis-cli -a --token reader:fixture-visible',
|
|
277
|
-
'redis-cli -a [redacted] [redacted]',
|
|
278
|
-
],
|
|
279
|
-
[
|
|
280
|
-
'redis-cli --pass --token reader:fixture-visible',
|
|
281
|
-
'redis-cli --pass [redacted] [redacted]',
|
|
282
|
-
],
|
|
283
|
-
[
|
|
284
|
-
'sshpass -p --token reader:fixture-visible ssh host',
|
|
285
|
-
'sshpass -p [redacted] [redacted] ssh host',
|
|
286
|
-
],
|
|
287
|
-
[
|
|
288
|
-
'curl --user --token reader:fixture-visible https://example.test',
|
|
289
|
-
'curl --user [redacted] [redacted] https://example.test',
|
|
290
|
-
],
|
|
291
|
-
[
|
|
292
|
-
'curl -u --token reader:fixture-visible https://example.test',
|
|
293
|
-
'curl -u [redacted] [redacted] https://example.test',
|
|
294
|
-
],
|
|
295
|
-
])
|
|
296
|
-
expect(sanitizeCommand(input)).toBe(output);
|
|
297
|
-
for (const input of [
|
|
298
|
-
'redis-cli --cluster-from-pass --token reader:fixture-visible',
|
|
299
|
-
'valkey-cli -a --api-key fixture-visible',
|
|
300
|
-
'REDIS-CLI.exe -a --token fixture-visible',
|
|
301
|
-
'sshpass -4p --token fixture-visible ssh host',
|
|
302
|
-
'openssl enc -k --token fixture-visible',
|
|
303
|
-
'openssl enc -K --secret fixture-visible',
|
|
304
|
-
'curl -U --token reader:fixture-visible https://example.test',
|
|
305
|
-
'curl --proxy-user --token reader:fixture-visible https://example.test',
|
|
306
|
-
'curl --oauth2-bearer --token fixture-visible https://example.test',
|
|
307
|
-
'curl --cookie --token fixture-visible https://example.test',
|
|
308
|
-
'curl -su --token reader:fixture-visible https://example.test',
|
|
309
|
-
'http -a --token reader:fixture-visible example.test',
|
|
310
|
-
'httpie --auth --token reader:fixture-visible example.test',
|
|
311
|
-
// Variants: separators, quoting, chains and wrappers.
|
|
312
|
-
'redis-cli -a\t--token\treader:fixture-visible',
|
|
313
|
-
'redis-cli -a --token\nreader:fixture-visible',
|
|
314
|
-
'redis-cli -a \\\n --token \\\n reader:fixture-visible',
|
|
315
|
-
'redis-cli -a "--token" reader:fixture-visible',
|
|
316
|
-
"curl -u '--api-key' reader:fixture-visible https://example.test",
|
|
317
|
-
'curl -u --token --client-secret fixture-visible https://example.test',
|
|
318
|
-
"sh -c 'redis-cli -a --token reader:fixture-visible'",
|
|
319
|
-
'bash -c "curl -u --token reader:fixture-visible https://example.test"',
|
|
320
|
-
'echo $(sshpass -p --token fixture-visible ssh host)',
|
|
321
|
-
'echo `redis-cli -a --token fixture-visible`',
|
|
322
|
-
'sudo -u root redis-cli -a --token fixture-visible',
|
|
323
|
-
'env FOO=1 curl --user --token reader:fixture-visible https://example.test',
|
|
324
|
-
// A curl config field takes the flag too, on a later line as well.
|
|
325
|
-
'echo -- --pass --token fixture-visible | curl -K -',
|
|
326
|
-
'echo -- -u --token reader:fixture-visible | curl -K -',
|
|
327
|
-
'echo -- oauth2-bearer --token fixture-visible | curl -K -',
|
|
328
|
-
'echo -- oauth2-bearer --access-token\nfixture-visible | curl -K -',
|
|
329
|
-
])
|
|
330
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
331
|
-
});
|
|
332
|
-
it('leaves a header name in a credential chain for the header rules to read', () => {
|
|
333
|
-
// A chain that redacted `Cookie:` would hide the cookie value from the
|
|
334
|
-
// header rule, which reads the name and the next word together.
|
|
335
|
-
for (const [input, output] of [
|
|
336
|
-
[
|
|
337
|
-
'redis-cli -a --token Cookie: session=fixture-visible',
|
|
338
|
-
'redis-cli -a [redacted] Cookie: [redacted]',
|
|
339
|
-
],
|
|
340
|
-
[
|
|
341
|
-
'curl -u --token Set-Cookie: session=fixture-visible',
|
|
342
|
-
'curl -u [redacted] Set-Cookie: [redacted]',
|
|
343
|
-
],
|
|
344
|
-
[
|
|
345
|
-
'x --password --token Cookie: session=fixture-visible',
|
|
346
|
-
'x --password [redacted] Cookie: [redacted]',
|
|
347
|
-
],
|
|
348
|
-
[
|
|
349
|
-
'x --password --token\nCookie: session=fixture-visible',
|
|
350
|
-
'x --password [redacted]\nCookie: [redacted]',
|
|
351
|
-
],
|
|
352
|
-
[
|
|
353
|
-
'redis-cli -a --token Authorization: Bearer fixture-visible',
|
|
354
|
-
'redis-cli -a [redacted] Authorization: [redacted]',
|
|
355
|
-
],
|
|
356
|
-
])
|
|
357
|
-
expect(sanitizeCommand(input)).toBe(output);
|
|
358
|
-
for (const input of [
|
|
359
|
-
'echo -- --pass --token Cookie: session=fixture-visible | curl -K -',
|
|
360
|
-
"bash -c 'redis-cli -a --token Cookie: session=fixture-visible'",
|
|
361
|
-
'redis-cli -a --token cookie:session=fixture-visible',
|
|
362
|
-
])
|
|
363
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
364
|
-
});
|
|
365
|
-
it('keeps what main redacts and kept around a tool password flag', () => {
|
|
366
|
-
// The tool flag's own value is still redacted when no chain follows it.
|
|
367
|
-
expect(sanitizeCommand('redis-cli -a fixture-secret ping')).toBe('redis-cli -a [redacted] ping');
|
|
368
|
-
expect(sanitizeCommand('curl -u reader:fixture-secret https://example.test')).toBe('curl -u [redacted] https://example.test');
|
|
369
|
-
// A swallowed word that is not a credential flag ends the chain: a Redis
|
|
370
|
-
// command after the password stays readable.
|
|
371
|
-
expect(sanitizeCommand('redis-cli -a --verbose ping')).toBe('redis-cli -a [redacted] ping');
|
|
372
|
-
// So does a flag that carries its own value, and an operator.
|
|
373
|
-
expect(sanitizeCommand('redis-cli -a --token=fixture-visible ping')).toBe('redis-cli -a [redacted] ping');
|
|
374
|
-
expect(sanitizeCommand('redis-cli -a --token; echo fixture-kept')).toBe('redis-cli -a [redacted]; echo fixture-kept');
|
|
375
|
-
// `--pass` is not a credential key, so the word after it is not its value.
|
|
376
|
-
expect(sanitizeCommand('redis-cli -a --pass ping')).toBe('redis-cli -a [redacted] ping');
|
|
377
|
-
// Flags that never swallow the next word as a credential still leave it
|
|
378
|
-
// to the space-separated rule.
|
|
379
|
-
expect(sanitizeCommand('mysql --password --token fixture-visible')).toBe('mysql --password [redacted] [redacted]');
|
|
380
|
-
expect(sanitizeCommand('openssl enc -pass --token fixture-visible')).toBe('openssl enc -pass --token [redacted]');
|
|
381
|
-
expect(sanitizeCommand('curl -x --token fixture-visible https://example.test')).toBe('curl -x --token [redacted] https://example.test');
|
|
382
|
-
});
|
|
383
|
-
// A credential flag pasted where a command starts runs no program, but the
|
|
384
|
-
// word after it is still the flag's value, however the flag is quoted.
|
|
385
|
-
it.each([
|
|
386
|
-
"'--token' fixture-visible",
|
|
387
|
-
'"--password" fixture-visible',
|
|
388
|
-
String.raw `\--token fixture-visible`,
|
|
389
|
-
"x\n'--token' fixture-visible",
|
|
390
|
-
"redis-cli -a\n'--token' fixture-visible",
|
|
391
|
-
"x; '--token' fixture-visible",
|
|
392
|
-
'x | "--api-key" fixture-visible',
|
|
393
|
-
'sh -c "\'--token\' fixture-visible"',
|
|
394
|
-
])('redacts the value of a credential flag that starts a command: %s', (input) => {
|
|
395
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
396
|
-
});
|
|
397
|
-
// A backtick substitution that closes onto a credential flag: both the
|
|
398
|
-
// value inside the body and the flag's value after it are credentials.
|
|
399
|
-
it.each([
|
|
400
|
-
'x `--token fixture-visible`--client-secret fixture-visible',
|
|
401
|
-
"x `'--token' fixture-visible`--client-secret fixture-visible",
|
|
402
|
-
'`--token fixture-visible`--client-secret fixture-visible',
|
|
403
|
-
"'--token' fixture-visible`echo a`--client-secret fixture-visible",
|
|
404
|
-
`;"--auth-token" 'fixture-visible'\`"--"passwd 'fixture-visible'\`--client-secret 'Cookie: fixture-visible' | tee out`,
|
|
405
|
-
])('redacts both values around a substitution that closes onto a flag: %s', (input) => {
|
|
406
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
407
|
-
});
|
|
408
|
-
// A nested word ends where the enclosing command may go on, so a header at
|
|
409
|
-
// its end is left for the enclosing header rules.
|
|
410
|
-
it.each([
|
|
411
|
-
`echo '"--token" 'Cookie: sid=fixture-visible`,
|
|
412
|
-
// A substitution rewritten inside a double quote that never closes fails
|
|
413
|
-
// closed from the quote, so the credential argument keeps its extent.
|
|
414
|
-
'redis-cli -a "x fixture-visible`"--token" "other"`',
|
|
415
|
-
'redis-cli -a "x fixture-visible`--token other`',
|
|
416
|
-
])('keeps redacting around a nested or unterminated word: %s', (input) => {
|
|
417
|
-
expect(sanitizeCommand(input)).not.toContain('fixture-visible');
|
|
418
|
-
});
|
|
419
|
-
it('redacts a header a credential flag takes when nothing follows it', () => {
|
|
420
|
-
// No header rule can read on past an operator, so the word is the value.
|
|
421
|
-
expect(sanitizeCommand("x --token --password 'Cookie: fixture-visible' | tee out")).not.toContain('fixture-visible');
|
|
422
|
-
});
|
|
423
|
-
it('reads a quoted credential flag as the unquoted one', () => {
|
|
424
|
-
// A header after the flag is redacted by the header rules first, as it is
|
|
425
|
-
// after an unquoted flag.
|
|
426
|
-
for (const flag of ["'--client-secret'", '--client-secret'])
|
|
427
|
-
expect(sanitizeCommand(`${flag} Authorization: Bearer fixture-visible`)).not.toContain('fixture-visible');
|
|
428
|
-
// A word that is not a credential flag still names the program.
|
|
429
|
-
expect(sanitizeCommand("'--verbose' fixture-kept")).toBe("'--verbose' fixture-kept");
|
|
430
|
-
expect(sanitizeCommand("'--output' fixture-kept")).toBe("'--output' fixture-kept");
|
|
431
|
-
// An operator ends the flag's command, so the next command keeps its words.
|
|
432
|
-
expect(sanitizeCommand("'--token'; echo fixture-kept")).toBe("'--token'; echo fixture-kept");
|
|
433
|
-
});
|
|
434
|
-
it('redacts token-only URL userinfo and URL query credentials', () => {
|
|
435
|
-
for (const input of [
|
|
436
|
-
'curl https://fixture-userinfo@example.test/',
|
|
437
|
-
'curl "https://example.test/?api_key=fixture-query&view=summary"',
|
|
438
|
-
]) {
|
|
439
|
-
const output = sanitizeCommand(input);
|
|
440
|
-
expect(output).not.toContain('fixture-userinfo');
|
|
441
|
-
expect(output).not.toContain('fixture-query');
|
|
442
|
-
expect(output).toContain('example.test');
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
it('strips here-string file contents while retaining command evidence', () => {
|
|
446
|
-
const output = sanitizeCommand(`cat <<< 'fixturePrivateSource second word' > output.txt`);
|
|
447
|
-
expect(output).not.toContain('fixturePrivateSource');
|
|
448
|
-
expect(output).toContain('cat <<<');
|
|
449
|
-
expect(output).toContain('output.txt');
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
describe('review command-evidence regressions', () => {
|
|
453
|
-
it('preserves the host after a cookie header and still redacts query credentials', () => {
|
|
454
|
-
const text = sanitizeCommand("curl -H 'Cookie: session=fixture-cookie' https://example.test/?api_key=fixture-query");
|
|
455
|
-
expect(text).not.toContain('fixture-cookie');
|
|
456
|
-
expect(text).not.toContain('fixture-query');
|
|
457
|
-
expect(text).toContain('example.test');
|
|
458
|
-
});
|
|
459
|
-
it.each([
|
|
460
|
-
'sort -u fixtures/input.txt',
|
|
461
|
-
'git add -u .',
|
|
462
|
-
'grep -b pattern src/app.ts',
|
|
463
|
-
'ps -u root',
|
|
464
|
-
'docker run --user 1000:1000',
|
|
465
|
-
])('preserves non-HTTP flags: %s', (command) => {
|
|
466
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
467
|
-
});
|
|
468
|
-
it('redacts HTTP credentials after environment assignments and command separators', () => {
|
|
469
|
-
const credential = ['fixture-user', 'fixture-password'].join(':');
|
|
470
|
-
const text = sanitizeCommand(`SORT=1 sort -u input.txt && env MODE=test /usr/bin/curl -u '${credential}' https://example.test`);
|
|
471
|
-
expect(text).toContain('sort -u input.txt');
|
|
472
|
-
expect(text).not.toContain('fixture-password');
|
|
473
|
-
expect(text).toContain('example.test');
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
describe('HTTPie authentication evidence', () => {
|
|
477
|
-
it.each(['-a ', '-a', '-a=', '--auth ', '--auth='])('redacts auth in the %s form', (flag) => {
|
|
478
|
-
const output = sanitizeCommand(`http ${flag}'fixture-user:fixture-password' example.test`);
|
|
479
|
-
expect(output).not.toContain('fixture-password');
|
|
480
|
-
expect(output).toContain('example.test');
|
|
481
|
-
});
|
|
482
|
-
it('preserves the auth type while redacting a bearer token on https', () => {
|
|
483
|
-
const output = sanitizeCommand('https -A bearer -a fixture-token example.test');
|
|
484
|
-
expect(output).not.toContain('fixture-token');
|
|
485
|
-
expect(output).toContain('-A bearer');
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
describe('credential syntax at the command upload boundary', () => {
|
|
489
|
-
it.each([
|
|
490
|
-
"bash -c 'API_TOKEN=x curl -x reader:fixture-value@proxy.test:3128 https://example.test'",
|
|
491
|
-
"bash -c 'password=x; https_proxy=reader:fixture-value@proxy.test:3128 curl https://example.test'",
|
|
492
|
-
"su -c 'API_TOKEN=x curl -x reader:fixture-value@proxy.test:3128 https://example.test'",
|
|
493
|
-
"env 'PASSWORD=fixture value' curl https://example.test",
|
|
494
|
-
"bash -c '{ curl -x reader:fixture-value@proxy.test:3128 https://example.test; }'",
|
|
495
|
-
"echo 'proxy = reader:fixture-value@proxy.test:3128' | curl -K - https://example.test",
|
|
496
|
-
"curl -K <(echo 'proxy = reader:fixture-value@proxy.test:3128') https://example.test",
|
|
497
|
-
"printf '%s\\n' 'proxy=reader:fixture-value@proxy.test:3128' | curl --config - https://example.test",
|
|
498
|
-
])('redacts assignment-led scripts and inline proxy configuration: %s', (command) => {
|
|
499
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
500
|
-
});
|
|
501
|
-
it.each([
|
|
502
|
-
'echo \'proxy-user = "reader:fixture-password"\' | curl -K - https://example.test',
|
|
503
|
-
'echo \'user = "reader:fixture-password"\' | curl -K - https://example.test',
|
|
504
|
-
'echo \'proxy-user = "reader:fixture-password"\' > cfg && curl -K cfg https://example.test',
|
|
505
|
-
'curl -K <(echo \'user = "reader:fixture-password"\') https://example.test',
|
|
506
|
-
"printf '%s\\n' 'user: reader:fixture-password' | curl -K - https://example.test",
|
|
507
|
-
'echo \'cookie = "SID=fixture-password"\' | curl -K - https://example.test',
|
|
508
|
-
'echo \'cert = "client.pem:fixture-password"\' | curl -K - https://example.test',
|
|
509
|
-
"echo 'proxy-cert = client.pem:fixture-password' | curl -K - https://example.test",
|
|
510
|
-
])('redacts credential fields in inline curl configuration: %s', (command) => {
|
|
511
|
-
const output = sanitizeCommand(command);
|
|
512
|
-
expect(output).not.toContain('fixture');
|
|
513
|
-
expect(output).toContain('[redacted]');
|
|
514
|
-
});
|
|
515
|
-
it.each([
|
|
516
|
-
'echo \'user = "reader"\' | curl -K - https://example.test',
|
|
517
|
-
'echo \'cert = "client.pem"\' | curl -K - https://example.test',
|
|
518
|
-
])('keeps curl configuration without a password: %s', (command) => {
|
|
519
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
520
|
-
});
|
|
521
|
-
it.each([
|
|
522
|
-
'sudo -E curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
523
|
-
'sudo -u deploy curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
524
|
-
'timeout --preserve-status 10 curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
525
|
-
'nice -n 10 curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
526
|
-
'stdbuf -oL curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
527
|
-
'xargs curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
528
|
-
'env -S "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
529
|
-
"su -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
530
|
-
'sudo -s "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
531
|
-
String.raw `env -S "sh -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'"`,
|
|
532
|
-
'bash -c "$(echo curl -x reader:fixture-password@proxy.test:3128 https://example.test)"',
|
|
533
|
-
"bash -c $'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
534
|
-
String.raw `bash -c $'true\ncurl -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
535
|
-
String.raw `bash -c $'\x63url -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
536
|
-
String.raw `bash -c $'echo \'ready\'; \x63url -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
537
|
-
])('redacts wrapper and encoded-script credentials: %s', (command) => {
|
|
538
|
-
const output = sanitizeCommand(command);
|
|
539
|
-
expect(output).not.toContain('fixture-password');
|
|
540
|
-
expect(output).toContain('proxy.test:3128');
|
|
541
|
-
expect(output).toContain('example.test');
|
|
542
|
-
});
|
|
543
|
-
it.each([
|
|
544
|
-
"sh -c 'export https_proxy=reader:fixture-password@proxy.test:3128; curl https://example.test'",
|
|
545
|
-
"sh -c 'cd /tmp; https_proxy=reader:fixture-password@proxy.test:3128 curl https://example.test'",
|
|
546
|
-
"eval 'export https_proxy=reader:fixture-password@proxy.test:3128' ; curl https://example.test",
|
|
547
|
-
"bash -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
548
|
-
'bash -lc "curl -sSx reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
549
|
-
'sudo bash -c "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
550
|
-
String.raw `bash -c 'sh -c "curl -x reader:fixture-password@proxy.test:3128 https://example.test"'`,
|
|
551
|
-
"bash -c 'https_proxy=reader:fixture-password@proxy.test:3128 curl -U reader:fixture-other https://example.test'",
|
|
552
|
-
'eval curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
553
|
-
])('redacts credentials inside quoted scripts: %s', (command) => {
|
|
554
|
-
const output = sanitizeCommand(command);
|
|
555
|
-
expect(output).not.toContain('fixture-password');
|
|
556
|
-
expect(output).not.toContain('fixture-other');
|
|
557
|
-
expect(output).toContain('proxy.test:3128');
|
|
558
|
-
expect(output).toContain('example.test');
|
|
559
|
-
});
|
|
560
|
-
it('bounds deeply nested interpreter scripts without retaining credentials', () => {
|
|
561
|
-
let command = 'curl -x reader:fixture-password@proxy.test:3128 https://example.test';
|
|
562
|
-
for (let depth = 0; depth < 12; depth += 1)
|
|
563
|
-
command = `bash -c ${JSON.stringify(command)}`;
|
|
564
|
-
const start = performance.now();
|
|
565
|
-
expect(sanitizeCommand(command)).not.toContain('fixture-password');
|
|
566
|
-
expect(performance.now() - start).toBeLessThan(2000);
|
|
567
|
-
});
|
|
568
|
-
it.each(['--pass ', '--pass=', '--proxy-pass ', '--proxy-pass='])('redacts separate private-key passphrases via %s', (flag) => {
|
|
569
|
-
expect(sanitizeCommand(`curl ${flag}'fixture-secret' https://example.test`)).not.toContain('fixture-secret');
|
|
570
|
-
});
|
|
571
|
-
it.each([
|
|
572
|
-
'curl reader:fixture-password@internal.test/api',
|
|
573
|
-
'curl -osx reader:fixture-password@internal.test/api',
|
|
574
|
-
'curl -- reader:fixture-password@internal.test/api',
|
|
575
|
-
"curl 'reader:fixture-password@internal.test/api'",
|
|
576
|
-
"bash -c 'curl reader:fixture-password@internal.test/api'",
|
|
577
|
-
])('redacts HTTP-client scheme-less URL credentials: %s', (command) => {
|
|
578
|
-
const output = sanitizeCommand(command);
|
|
579
|
-
expect(output).not.toContain('fixture-password');
|
|
580
|
-
expect(output).toContain('internal.test/api');
|
|
581
|
-
});
|
|
582
|
-
it('preserves benign quoted scripts and unrelated userinfo-shaped arguments', () => {
|
|
583
|
-
for (const command of [
|
|
584
|
-
"bash -c 'sort -u input.txt; curl -sS https://example.test'",
|
|
585
|
-
"docker pull 'image:tag@sha256:abc123'",
|
|
586
|
-
"git clone 'git@github.test:org/repo'",
|
|
587
|
-
])
|
|
588
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
589
|
-
});
|
|
590
|
-
it.each([
|
|
591
|
-
['x', 'reader:fixture-password@proxy.test:3128'],
|
|
592
|
-
['u', 'reader:fixture-password'],
|
|
593
|
-
['U', 'reader:fixture-password'],
|
|
594
|
-
['b', 'session=fixture-cookie'],
|
|
595
|
-
['E', 'client.pem:fixture-passphrase'],
|
|
596
|
-
['H', 'X-Api-Key: fixture-header'],
|
|
597
|
-
['d', '%70assword=fixture-form'],
|
|
598
|
-
['F', '%70assword=fixture-form'],
|
|
599
|
-
])('redacts curl clustered argument flag %s', (flag, value) => {
|
|
600
|
-
for (const prefix of ['s', 'sS', 'qkLf']) {
|
|
601
|
-
for (const space of ['', ' ']) {
|
|
602
|
-
const output = sanitizeCommand(`curl -${prefix}${flag}${space}'${value}' https://example.test`);
|
|
603
|
-
expect(output).not.toContain('fixture');
|
|
604
|
-
expect(output).toContain(`-${prefix}${flag}`);
|
|
605
|
-
expect(output).toContain('https://example.test');
|
|
606
|
-
if (flag === 'x')
|
|
607
|
-
expect(output).toContain('proxy.test:3128');
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
it('does not interpret an argument-taking flag value as more curl flags', () => {
|
|
612
|
-
const command = "curl -osxreport.txt -sH'Accept: application/json' https://example.test";
|
|
613
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
614
|
-
const proxy = 'curl -sSxproxy.test:3128 https://example.test';
|
|
615
|
-
expect(sanitizeCommand(proxy)).toBe(proxy);
|
|
616
|
-
});
|
|
617
|
-
it.each(['-U ', '-U', '--proxy-user='])('redacts curl proxy credentials via %s', (flag) => {
|
|
618
|
-
expect(sanitizeCommand(`curl ${flag}'user:fixture proxy password' https://example.test`)).not.toContain('fixture');
|
|
619
|
-
});
|
|
620
|
-
it.each(['--oauth2-bearer ', '--oauth2-bearer='])('redacts curl OAuth bearer credentials via %s', (flag) => {
|
|
621
|
-
expect(sanitizeCommand(`curl ${flag}fixture-bearer https://example.test`)).not.toContain('fixture');
|
|
622
|
-
});
|
|
623
|
-
it.each(['-E ', '-E', '--cert ', '--cert=', '--proxy-cert='])('redacts certificate passphrases via %s', (flag) => {
|
|
624
|
-
expect(sanitizeCommand(`curl ${flag}'client.pem:fixture passphrase' https://example.test`)).not.toContain('fixture');
|
|
625
|
-
});
|
|
626
|
-
it.each(['-H ', '-H', '--header ', '--header=', '--proxy-header='])('redacts custom credential headers via %s', (flag) => {
|
|
627
|
-
expect(sanitizeCommand(`curl ${flag}'X-Api-Key: fixture-header-secret' https://example.test`)).not.toContain('fixture');
|
|
628
|
-
expect(sanitizeCommand(`curl ${flag}'Accept: application/json' https://example.test`)).toContain('Accept: application/json');
|
|
629
|
-
});
|
|
630
|
-
it.each(['-d ', '-d', '--data=', '--data-urlencode ', '-F ', '--form-string='])('redacts credential form bodies via %s', (flag) => {
|
|
631
|
-
expect(sanitizeCommand(`curl ${flag}'password=fixture%20password&name=reader' https://example.test`)).not.toContain('fixture');
|
|
632
|
-
expect(sanitizeCommand(`curl ${flag}'%70assword=fixture-password' https://example.test`)).not.toContain('fixture');
|
|
633
|
-
expect(sanitizeCommand(`curl ${flag}'view=summary' https://example.test`)).toContain('view=summary');
|
|
634
|
-
});
|
|
635
|
-
it.each([
|
|
636
|
-
"'--password'",
|
|
637
|
-
'"--password"',
|
|
638
|
-
String.raw `--pass\word`,
|
|
639
|
-
"'--password=fixture password'",
|
|
640
|
-
])('redacts shell-encoded credential flags %s', (flag) => {
|
|
641
|
-
const command = flag.includes('=')
|
|
642
|
-
? `tool ${flag}`
|
|
643
|
-
: `tool ${flag} 'fixture password' --verbose`;
|
|
644
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
645
|
-
});
|
|
646
|
-
it('redacts OAuth fragment credentials while retaining the address', () => {
|
|
647
|
-
const output = sanitizeCommand('curl "https://example.test/#access_token=fixture-fragment&view=summary"');
|
|
648
|
-
expect(output).not.toContain('fixture');
|
|
649
|
-
expect(output).toContain('https://example.test/');
|
|
650
|
-
expect(output).toContain('view=summary');
|
|
651
|
-
});
|
|
652
|
-
it.each(['-x ', '-x', '--proxy ', '--proxy=', '--proxy1.0 '])('redacts scheme-less proxy userinfo via %s', (flag) => {
|
|
653
|
-
const output = sanitizeCommand(`curl ${flag}'reader:fixture-password@proxy.test:3128' https://example.test`);
|
|
654
|
-
expect(output).not.toContain('fixture-password');
|
|
655
|
-
expect(output).toContain('proxy.test:3128');
|
|
656
|
-
expect(sanitizeCommand(`curl ${flag}proxy.test:3128 https://example.test`)).toContain('proxy.test:3128');
|
|
657
|
-
expect(sanitizeCommand(`curl ${flag}'http://reader:fixture-password@proxy.test:3128' https://example.test`)).not.toContain('fixture-password');
|
|
658
|
-
});
|
|
659
|
-
describe('proxy environment assignments', () => {
|
|
660
|
-
it.each([
|
|
661
|
-
"env 'https_proxy=reader:fixture-password@proxy.test:3128' curl https://example.test",
|
|
662
|
-
'env "ALL_PROXY=reader:fixture-password@proxy.test:3128" curl https://example.test',
|
|
663
|
-
String.raw `env https_pr\oxy=reader:fixture-password@proxy.test:3128 curl https://example.test`,
|
|
664
|
-
])('redacts shell-encoded complete proxy assignments: %s', (command) => {
|
|
665
|
-
const output = sanitizeCommand(command);
|
|
666
|
-
expect(output).not.toContain('fixture-password');
|
|
667
|
-
expect(output).toContain('proxy.test:3128');
|
|
668
|
-
});
|
|
669
|
-
const names = ['https_proxy', 'http_proxy', 'HTTPS_PROXY', 'ALL_PROXY', 'all_proxy'];
|
|
670
|
-
const values = [
|
|
671
|
-
'reader:fixture-password@proxy.test:3128',
|
|
672
|
-
"'reader:fixture-password@proxy.test:3128'",
|
|
673
|
-
'"reader:fixture-password@proxy.test:3128"',
|
|
674
|
-
"'reader:fixture password@proxy.test:3128'",
|
|
675
|
-
];
|
|
676
|
-
for (const name of names) {
|
|
677
|
-
it.each(values)(`redacts scheme-less userinfo in ${name}=%s`, (value) => {
|
|
678
|
-
for (const prefix of ['', 'export ', 'sudo -E ', 'env ']) {
|
|
679
|
-
const output = sanitizeCommand(`${prefix}${name}=${value} curl https://example.test`);
|
|
680
|
-
expect(output).not.toContain('fixture');
|
|
681
|
-
expect(output).toContain('reader:');
|
|
682
|
-
expect(output).toContain('proxy.test:3128');
|
|
683
|
-
expect(output).toContain('curl https://example.test');
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
it('keeps a proxy value without credentials intact', () => {
|
|
688
|
-
for (const command of [
|
|
689
|
-
'https_proxy=proxy.test:3128 curl https://example.test',
|
|
690
|
-
"ALL_PROXY='socks5://proxy.test:1080' curl https://example.test",
|
|
691
|
-
'no_proxy=localhost,127.0.0.1 curl https://example.test',
|
|
692
|
-
])
|
|
693
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
694
|
-
});
|
|
695
|
-
it('leaves unrelated scheme-less @ text alone', () => {
|
|
696
|
-
for (const command of [
|
|
697
|
-
'docker pull nginx:1.25@sha256:0123456789abcdef',
|
|
698
|
-
'git clone git@github.com:org/repo.git',
|
|
699
|
-
])
|
|
700
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
701
|
-
});
|
|
702
|
-
});
|
|
703
|
-
describe('OpenSSL and sshpass credential arguments', () => {
|
|
704
|
-
it.each([
|
|
705
|
-
'openssl enc -d -aes-256-cbc -pass pass:fixture-password -in f.enc',
|
|
706
|
-
"openssl rsa -passin 'pass:fixture password' -in key.pem",
|
|
707
|
-
'openssl pkcs12 -passout pass:fixture-password -out key.p12',
|
|
708
|
-
'openssl enc -aes-256-cbc -k fixture-password',
|
|
709
|
-
'openssl enc -aes-256-cbc -pass=pass:fixture-password',
|
|
710
|
-
'openssl enc -aes-256-cbc --pass pass:fixture-password',
|
|
711
|
-
'openssl enc -aes-256-cbc --pass=pass:fixture-password',
|
|
712
|
-
'openssl rsa --passin pass:fixture-password -in key.pem',
|
|
713
|
-
'openssl pkcs12 --passout pass:fixture-password -out key.p12',
|
|
714
|
-
'openssl enc -aes-256-cbc --k fixture-password',
|
|
715
|
-
'openssl pkcs12 -password pass:fixture-password',
|
|
716
|
-
"env 'SSHPASS=fixture password' sshpass -e ssh deploy@host",
|
|
717
|
-
"sudo -u root /usr/bin/openssl enc '-pass' pass:fixture-password",
|
|
718
|
-
"bash -c 'openssl enc -pass pass:fixture-password'",
|
|
719
|
-
'sshpass -p fixture-password ssh deploy@host git pull',
|
|
720
|
-
'sshpass -pfixture-password ssh deploy@host git pull',
|
|
721
|
-
"sshpass -vp'fixture password' ssh deploy@host",
|
|
722
|
-
'timeout 10 /usr/bin/sshpass -4vpfixture-password ssh deploy@host',
|
|
723
|
-
String.raw `sshpass -\p $'fixture\x20password' ssh deploy@host`,
|
|
724
|
-
"openssl passwd -1 'fixture-password'",
|
|
725
|
-
'openssl passwd -6 fixture-password',
|
|
726
|
-
'openssl passwd -salt pepper fixture-password',
|
|
727
|
-
'sudo openssl passwd -apr1 -salt x fixture-password fixture-second',
|
|
728
|
-
'openssl enc -aes-256-cbc -K 00fixture1122 -iv 00 -in f -out g',
|
|
729
|
-
'openssl enc -aes-256-cbc -K=00fixture1122 -in f -out g',
|
|
730
|
-
])('redacts direct passwords: %s', (command) => {
|
|
731
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
732
|
-
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
733
|
-
});
|
|
734
|
-
it.each([
|
|
735
|
-
'openssl enc -pass env:KEY_PASSWORD -in f.enc',
|
|
736
|
-
'openssl enc --pass env:KEY_PASSWORD -in f.enc',
|
|
737
|
-
'openssl rsa -passin file:password-source -in key.pem',
|
|
738
|
-
'openssl pkcs12 -passout fd:3',
|
|
739
|
-
'openssl enc -pass stdin',
|
|
740
|
-
'openssl enc -kfile password-source -in f.enc',
|
|
741
|
-
'sshpass -e ssh deploy@host',
|
|
742
|
-
'sshpass -f password-source ssh deploy@host',
|
|
743
|
-
'sshpass -d 3 ssh deploy@host',
|
|
744
|
-
'printf -p fixture-public',
|
|
745
|
-
'tool -pass pass:fixture-public',
|
|
746
|
-
'openssl passwd -stdin',
|
|
747
|
-
'openssl passwd -in password-source',
|
|
748
|
-
'openssl x509 -in cert.pem -noout -text',
|
|
749
|
-
])('preserves nonliteral sources and unrelated arguments: %s', (command) => {
|
|
750
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
751
|
-
});
|
|
752
|
-
});
|
|
753
|
-
describe('redis-cli and mysql password arguments', () => {
|
|
754
|
-
it.each([
|
|
755
|
-
'redis-cli -a fixture-password keys "*"',
|
|
756
|
-
'redis-cli -h cache -a fixture-password get k',
|
|
757
|
-
'redis-cli --pass fixture-password get k',
|
|
758
|
-
'redis-cli --pass=fixture-password get k',
|
|
759
|
-
'redis-cli -a "fixture password" get k',
|
|
760
|
-
'mysql -u root -pfixture-password -h db',
|
|
761
|
-
"mysql -u root -p'fixture password' db",
|
|
762
|
-
'mysql -pfixture-password db',
|
|
763
|
-
'mysql --password=fixture-password db',
|
|
764
|
-
'redis-cli --cluster import 127.0.0.1:7000 --cluster-from 127.0.0.1:6379 --cluster-from-pass fixture-password',
|
|
765
|
-
'redis-cli --cluster-from-pass=fixture-password --cluster info 127.0.0.1:6379',
|
|
766
|
-
'REDISCLI_AUTH=fixture-password redis-cli get k',
|
|
767
|
-
'MYSQL_PWD=fixture-password mysql -u root db',
|
|
768
|
-
'env MYSQL_PWD=fixture-password mysql db',
|
|
769
|
-
'redis-cli -u redis://:fixture-password@cache:6379 get k',
|
|
770
|
-
])('redacts a password argument: %s', (command) => {
|
|
771
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
772
|
-
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
773
|
-
});
|
|
774
|
-
it.each([
|
|
775
|
-
'redis-cli -h cache -p 6379 get k',
|
|
776
|
-
'redis-cli --user reader keys "*"',
|
|
777
|
-
'redis-cli SET artifact image:tag@sha256:digest',
|
|
778
|
-
'redis-cli SET contact user@example.com',
|
|
779
|
-
'mysql -u root -P 3306 db',
|
|
780
|
-
'mysql -u root -p -h db',
|
|
781
|
-
'mysql -p mydb',
|
|
782
|
-
'mysql -h db -p mydb',
|
|
783
|
-
'mysql --protocol tcp db',
|
|
784
|
-
])('preserves unrelated arguments: %s', (command) => {
|
|
785
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
786
|
-
});
|
|
787
|
-
});
|
|
788
|
-
describe('valkey-cli and MySQL-family password arguments', () => {
|
|
789
|
-
it.each([
|
|
790
|
-
'valkey-cli -a fixture-password get k',
|
|
791
|
-
'valkey-cli -h cache -a fixture-password get k',
|
|
792
|
-
'valkey-cli -a "fixture password" get k',
|
|
793
|
-
'valkey-cli --pass fixture-password get k',
|
|
794
|
-
'valkey-cli --pass=fixture-password get k',
|
|
795
|
-
'valkey-cli --cluster-from-pass=fixture-password --cluster info 127.0.0.1:6379',
|
|
796
|
-
'valkey-cli -u valkey://:fixture-password@cache:6379 get k',
|
|
797
|
-
'valkey-cli -u valkeys://:fixture-password@cache:6379 get k',
|
|
798
|
-
'redis-cli -u rediss://:fixture-password@cache:6379 get k',
|
|
799
|
-
'VALKEYCLI_AUTH=fixture-password valkey-cli get k',
|
|
800
|
-
'env VALKEYCLI_AUTH=fixture-password valkey-cli get k',
|
|
801
|
-
'mariadb -u root -pfixture-password -h db',
|
|
802
|
-
"mariadb -u root -p'fixture password' db",
|
|
803
|
-
'mariadb -pfixture-password db',
|
|
804
|
-
'mariadb --password=fixture-password db',
|
|
805
|
-
'mysqldump -u root -pfixture-password db',
|
|
806
|
-
"mysqldump -u root -p'fixture password' db",
|
|
807
|
-
'mysqldump --password=fixture-password db',
|
|
808
|
-
'mysqladmin -u root -pfixture-password ping',
|
|
809
|
-
'mysqladmin --password=fixture-password ping',
|
|
810
|
-
'mariadb-dump -u root -pfixture-password db',
|
|
811
|
-
'mariadb-dump --password=fixture-password db',
|
|
812
|
-
'mariadb-admin -u root -pfixture-password ping',
|
|
813
|
-
'mariadb-admin --password=fixture-password ping',
|
|
814
|
-
'valkey-cli.exe -a fixture-password get k',
|
|
815
|
-
'mariadb.exe -pfixture-password db',
|
|
816
|
-
'mysqldump.EXE -pfixture-password db',
|
|
817
|
-
'MYSQL.EXE -pfixture-password db',
|
|
818
|
-
'VALKEY-CLI.EXE -a fixture-password get k',
|
|
819
|
-
'MYSQL_PWD=fixture-password mariadb -u root db',
|
|
820
|
-
'env MYSQL_PWD=fixture-password mysqldump db',
|
|
821
|
-
])('redacts a password argument: %s', (command) => {
|
|
822
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
823
|
-
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
824
|
-
});
|
|
825
|
-
// A bare --password prompts in MySQL-family clients, so the next word is an
|
|
826
|
-
// operand. The tool-agnostic `--credential value` rule cannot tell that apart,
|
|
827
|
-
// so it redacts the operand: over-redaction, never a leak.
|
|
828
|
-
it.each([
|
|
829
|
-
'mysql --password appdb',
|
|
830
|
-
'mariadb --password appdb db',
|
|
831
|
-
'mysqldump --password appdb',
|
|
832
|
-
'mysqladmin --password ping',
|
|
833
|
-
'mariadb-dump --password appdb',
|
|
834
|
-
])('redacts, rather than leaks, the operand after a bare --password: %s', (command) => {
|
|
835
|
-
expect(sanitizeCommand(command)).not.toContain('appdb');
|
|
836
|
-
expect(sanitizeCommand(command)).not.toContain('ping');
|
|
837
|
-
});
|
|
838
|
-
it.each([
|
|
839
|
-
'valkey-cli -h cache -p 6379 get k',
|
|
840
|
-
'valkey-cli --user reader keys "*"',
|
|
841
|
-
'valkey-cli GET valkey://cache:6379',
|
|
842
|
-
'valkey-cli SET artifact image:tag@sha256:digest',
|
|
843
|
-
'valkey-cli SET contact user@example.com',
|
|
844
|
-
'mariadb -u root -P 3306 db',
|
|
845
|
-
'mariadb -u root -p -h db',
|
|
846
|
-
'mariadb -p mydb',
|
|
847
|
-
'mysqldump -p mydb',
|
|
848
|
-
'mysqldump --databases mydb',
|
|
849
|
-
'mysqladmin -u root -p ping',
|
|
850
|
-
// Unrelated tools keep their -p flags, including names that only start
|
|
851
|
-
// with a family member's name.
|
|
852
|
-
'tool -pfixture-public db',
|
|
853
|
-
'tool.exe -pfixture-public db',
|
|
854
|
-
'mysql_check -pfixture-public db',
|
|
855
|
-
'mariadb-dump-helper -pfixture-public db',
|
|
856
|
-
])('preserves unrelated arguments: %s', (command) => {
|
|
857
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
858
|
-
});
|
|
859
|
-
});
|
|
860
|
-
describe('inline curl configuration credentials', () => {
|
|
861
|
-
it.each([
|
|
862
|
-
`echo 'us''er = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
863
|
-
String.raw `echo us\er=reader:fixture-password | curl -K - file:///dev/null`,
|
|
864
|
-
String.raw `echo $'u\x73er = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
865
|
-
])('decodes shell-encoded config names: %s', (command) => {
|
|
866
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
867
|
-
});
|
|
868
|
-
it.each([
|
|
869
|
-
`printf 'user = "reader:%s"\\n' fixture-password | curl -K - file:///dev/null`,
|
|
870
|
-
`printf 'cookie = "%s"\\n' SID=fixture-password | curl -K - file:///dev/null`,
|
|
871
|
-
`printf '%s = %s\\n' user reader:fixture-password | curl -K - file:///dev/null`,
|
|
872
|
-
])('redacts printf credential substitutions: %s', (command) => {
|
|
873
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
874
|
-
});
|
|
875
|
-
it.each([
|
|
876
|
-
`bash -c 'echo "user = reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
877
|
-
`env -S 'printf "user = reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
878
|
-
])('redacts wrapped config producers: %s', (command) => {
|
|
879
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
880
|
-
});
|
|
881
|
-
it.each([
|
|
882
|
-
`echo 'user "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
883
|
-
`echo 'user: "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
884
|
-
`echo '--user = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
885
|
-
`echo '-u "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
886
|
-
])('redacts alternate curl config separators: %s', (command) => {
|
|
887
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
888
|
-
});
|
|
889
|
-
it.each([
|
|
890
|
-
`echo 'proxy-user = "reader:fixture-password"' | curl -K - https://example.test`,
|
|
891
|
-
`echo 'user = "reader:fixture password"' | curl -K - https://example.test`,
|
|
892
|
-
`echo 'proxy-user="reader:fixture-password"' > cfg && curl -K cfg https://example.test`,
|
|
893
|
-
`curl -K <(echo 'user = "reader:fixture-password"') https://example.test`,
|
|
894
|
-
`echo 'cookie = "SID=fixture-password"' | curl -K - https://example.test`,
|
|
895
|
-
`echo 'cert = "client.pem:fixture-password"' | curl -K - https://example.test`,
|
|
896
|
-
`echo 'proxy-cert = "client.pem:fixture-password"' | curl -K - https://example.test`,
|
|
897
|
-
`printf '%s\\n' 'oauth2-bearer = "fixture-password"' | curl -K - https://example.test`,
|
|
898
|
-
`printf '%s\\n' 'pass = "fixture-password"' | curl -K - https://example.test`,
|
|
899
|
-
`echo 'proxy-pass = "fixture-password"' | curl -K - https://example.test`,
|
|
900
|
-
])('redacts credential field literals: %s', (command) => {
|
|
901
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
902
|
-
});
|
|
903
|
-
it('keeps unrelated flags before a config producer', () => {
|
|
904
|
-
expect(sanitizeCommand(`sort -u public-file && echo 'user = reader:fixture-password' | curl -K - file:///dev/null`)).toContain('sort -u public-file');
|
|
905
|
-
});
|
|
906
|
-
it('retains noncredential curl configuration', () => {
|
|
907
|
-
const command = `echo 'proxy = proxy.test:3128' | curl -K - https://example.test`;
|
|
908
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
describe('OpenSSL positional passwords and raw keys', () => {
|
|
912
|
-
it('redacts a valid raw AES key while retaining the IV', () => {
|
|
913
|
-
const key = '00112233445566778899aabbccddeeff';
|
|
914
|
-
const iv = 'ffeeddccbbaa99887766554433221100';
|
|
915
|
-
const output = sanitizeCommand(`openssl enc -aes-128-cbc -K ${key} -iv ${iv} -in public-file`);
|
|
916
|
-
expect(output).not.toContain(key);
|
|
917
|
-
expect(output).toContain(iv);
|
|
918
|
-
});
|
|
919
|
-
it.each(['curl', 'openssl', 'sshpass'])('redacts a password named %s', (password) => {
|
|
920
|
-
expect(sanitizeCommand(`openssl passwd -1 '${password}'`)).toBe('openssl passwd -1 [redacted]');
|
|
921
|
-
});
|
|
922
|
-
it.each([
|
|
923
|
-
`openssl passwd -1 'fixture-password'`,
|
|
924
|
-
`openssl passwd -6 'fixture password'`,
|
|
925
|
-
`openssl passwd -salt public-salt fixture-password`,
|
|
926
|
-
`openssl passwd -5 fixture-password fixture-other-password`,
|
|
927
|
-
`openssl passwd -- fixture-password`,
|
|
928
|
-
`openssl enc -aes-256-cbc -K fixture-key -in public-file`,
|
|
929
|
-
`openssl enc -aes-256-cbc --K=fixture-key -in public-file`,
|
|
930
|
-
])('redacts key material: %s', (command) => {
|
|
931
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
932
|
-
});
|
|
933
|
-
it.each([
|
|
934
|
-
'openssl passwd -1 -salt public-salt -in password-source',
|
|
935
|
-
'openssl passwd -6 -stdin',
|
|
936
|
-
'openssl passwd -rand entropy-source -writerand entropy-out -stdin',
|
|
937
|
-
])('preserves password sources and salt: %s', (command) => {
|
|
938
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
939
|
-
});
|
|
940
|
-
});
|
|
941
|
-
describe('Codex command-redaction findings', () => {
|
|
942
|
-
it.each([
|
|
943
|
-
`printf 'variable = cred=reader:fixture-password\nexpand-user = {{cred}}\nurl = file:///dev/null\n' | curl -K -`,
|
|
944
|
-
`echo 'variable = cred=fixture-password\nexpand-oauth2-bearer = {{cred}}' | curl -K -`,
|
|
945
|
-
`printf '%s\\n' 'variable = cred=fixture-password' 'expand-header = "X-Api-Key: {{cred}}"' | curl -K -`,
|
|
946
|
-
`export CRED=fixture-password; printf 'variable = %%CRED\nexpand-user = {{CRED}}' | curl -K -`,
|
|
947
|
-
])('redacts expanded credentials in curl config inputs: %s', (command) => {
|
|
948
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
949
|
-
});
|
|
950
|
-
it.each([
|
|
951
|
-
"env 'P.W=fixture-password' openssl enc -pass env:P.W",
|
|
952
|
-
"env 'P-W=fixture-password' openssl enc -pass env:P-W",
|
|
953
|
-
"env 'P W=fixture-password' openssl enc -pass 'env:P W'",
|
|
954
|
-
])('redacts non-shell environment names: %s', (command) => {
|
|
955
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
956
|
-
});
|
|
957
|
-
it.each([
|
|
958
|
-
'openssl s_client -srppass fixture-password -connect example.test:443',
|
|
959
|
-
'openssl s_client -srppass=fixture-password -connect example.test:443',
|
|
960
|
-
'openssl cms -pwri_password fixture-password -encrypt -in /dev/null',
|
|
961
|
-
'openssl dgst -hmac fixture-password /dev/null',
|
|
962
|
-
])('redacts other OpenSSL password/key flags: %s', (command) => {
|
|
963
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
964
|
-
});
|
|
965
|
-
it('redacts CMS key bytes while keeping their public identifier', () => {
|
|
966
|
-
const key = '0'.repeat(32);
|
|
967
|
-
expect(sanitizeCommand(`openssl cms -secretkey ${key} -secretkeyid public-id`)).toBe('openssl cms -secretkey [redacted] -secretkeyid public-id');
|
|
968
|
-
});
|
|
969
|
-
it.each([
|
|
970
|
-
'curl -u reader:$(printf fixture-password) https://example.test',
|
|
971
|
-
'sshpass -p$(printf fixture-password) ssh deploy@host',
|
|
972
|
-
'openssl enc -pass pass:$(printf fixture-password)',
|
|
973
|
-
'curl -u reader:`printf fixture-password` https://example.test',
|
|
974
|
-
'sshpass -p`printf fixture-password` ssh deploy@host',
|
|
975
|
-
'curl -u reader:$(printf "%s" "$(printf fixture-password)") https://example.test',
|
|
976
|
-
"curl -u reader:$(printf ')%s' fixture-password) https://example.test",
|
|
977
|
-
'curl -u "reader:$(printf fixture-password)" https://example.test',
|
|
978
|
-
"bash -c 'curl -u reader:$(printf fixture-password) https://example.test'",
|
|
979
|
-
'curl -u reader:$(printf fixture-password',
|
|
980
|
-
])('redacts command-substitution bodies in credential words: %s', (command) => {
|
|
981
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
982
|
-
});
|
|
983
|
-
it.each([
|
|
984
|
-
'openssl rand -out passwd 16',
|
|
985
|
-
'openssl x509 -in passwd -noout -text',
|
|
986
|
-
'openssl cms -secretkeyid public-key-id -help',
|
|
987
|
-
`echo 'variable = host=example.test\nexpand-url = https://{{host}}' | curl -K -`,
|
|
988
|
-
])('preserves subcommand arguments and public config expansions: %s', (command) => {
|
|
989
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
990
|
-
});
|
|
991
|
-
it.each([
|
|
992
|
-
'curl --variable cred=reader:fixture-password --expand-user {{cred}} file:///dev/null',
|
|
993
|
-
'curl --variable=cred=reader:fixture-password --expand-proxy-user={{cred}} https://example.test',
|
|
994
|
-
"curl --variable 'cred=fixture password' --expand-oauth2-bearer '{{cred:trim}}' https://example.test",
|
|
995
|
-
'curl --variable cred=fixture-password --expand-header "X-Api-Key: {{cred}}" https://example.test',
|
|
996
|
-
'curl --variable cred=fixture-password --expand-data "password={{cred}}" https://example.test',
|
|
997
|
-
'curl --variable cred=fixture-password --expand-url "https://reader:{{cred}}@example.test"',
|
|
998
|
-
'curl --variable cred=fixture-password --expand-url "https://example.test/#access_token={{cred}}"',
|
|
999
|
-
'curl --variable cred=fixture-password --expand-variable login=reader:{{cred}} --expand-user {{login}} file:///dev/null',
|
|
1000
|
-
'curl --variable raw=reader:fixture-password@example.test --expand-variable alias={{raw}} --expand-url {{alias}}',
|
|
1001
|
-
'curl --variable raw=reader:fixture-password@example.test --expand-url {{raw}} --variable raw=example.test',
|
|
1002
|
-
'CRED=fixture-password curl --variable %CRED --expand-oauth2-bearer {{CRED}} https://example.test',
|
|
1003
|
-
'curl --variable %CRED=fixture-password --expand-oauth2-bearer {{CRED}} https://example.test',
|
|
1004
|
-
'curl --variable cred[0-99]=fixture-password --expand-oauth2-bearer {{cred}} https://example.test',
|
|
1005
|
-
])('redacts expanded curl credentials at their source: %s', (command) => {
|
|
1006
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1007
|
-
});
|
|
1008
|
-
it.each([
|
|
1009
|
-
'P=fixture-password openssl enc -aes-256-cbc -pass env:P -in /dev/null -out /dev/null',
|
|
1010
|
-
"env 'P=fixture password' openssl enc -pass env:P",
|
|
1011
|
-
'P+=fixture-password openssl enc -pass env:P',
|
|
1012
|
-
'printf fixture-password | openssl enc -pass stdin',
|
|
1013
|
-
'printf "%s\\n" fixture-password | openssl enc -pass stdin',
|
|
1014
|
-
"printf fixture-password | sh -c 'openssl enc -pass stdin'",
|
|
1015
|
-
'printf fixture-password > pass-source; openssl enc -pass file:pass-source',
|
|
1016
|
-
'openssl enc -pass fd:3 3< <(printf fixture-password)',
|
|
1017
|
-
'openssl enc -pass file:<(printf fixture-password)',
|
|
1018
|
-
'openssl enc -pass stdin <<< fixture-password',
|
|
1019
|
-
'printf fixture-password > pass-source; openssl enc -kfile pass-source',
|
|
1020
|
-
'printf fixture-password | openssl passwd -stdin',
|
|
1021
|
-
'printf fixture-password > pass-source; openssl passwd -in pass-source',
|
|
1022
|
-
'P=$(printf fixture-password) openssl enc -pass env:P',
|
|
1023
|
-
])('redacts literal OpenSSL passphrase producers: %s', (command) => {
|
|
1024
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1025
|
-
});
|
|
1026
|
-
it.each([
|
|
1027
|
-
'HTTPS_PROXY+=reader:fixture-password@proxy.test:3128 curl https://example.test',
|
|
1028
|
-
"ALL_PROXY+='reader:fixture password@proxy.test:3128' curl https://example.test",
|
|
1029
|
-
])('redacts appended proxy assignments: %s', (command) => {
|
|
1030
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1031
|
-
expect(sanitizeCommand(command)).toContain('proxy.test:3128');
|
|
1032
|
-
});
|
|
1033
|
-
it.each([
|
|
1034
|
-
'openssl passwd -provider default -stdin',
|
|
1035
|
-
'openssl passwd -provider-path /public/providers -provider default -stdin',
|
|
1036
|
-
'openssl passwd -propquery provider=default -stdin',
|
|
1037
|
-
'openssl passwd -provparam public=value -stdin',
|
|
1038
|
-
'echo "user = reader:public-value"; ssh -K host; curl https://example.test',
|
|
1039
|
-
'curl https://example.test; ssh -K host; echo "user = reader:public-value"',
|
|
1040
|
-
'echo "user = reader:public-value"; curl -o -K https://example.test',
|
|
1041
|
-
'curl --variable host=example.test --expand-url "https://{{host}}/public"',
|
|
1042
|
-
'curl --variable name=public --expand-output {{name}} https://example.test',
|
|
1043
|
-
'echo public-value; openssl enc -pass stdin',
|
|
1044
|
-
'echo public-value; openssl enc -pass env:P',
|
|
1045
|
-
])('preserves public option values and unrelated commands: %s', (command) => {
|
|
1046
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
1047
|
-
});
|
|
1048
|
-
it.each([
|
|
1049
|
-
'TOOL=/usr/bin/openssl curl -u reader:fixture-password https://example.test',
|
|
1050
|
-
'sudo -u openssl curl -u reader:fixture-password https://example.test',
|
|
1051
|
-
'env -u openssl curl -u reader:fixture-password https://example.test',
|
|
1052
|
-
'/usr/bin/time -o openssl curl -u reader:fixture-password https://example.test',
|
|
1053
|
-
'xargs -a openssl curl -u reader:fixture-password https://example.test',
|
|
1054
|
-
])('does not select a client from wrapper option values: %s', (command) => {
|
|
1055
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1056
|
-
});
|
|
1057
|
-
it.each([
|
|
1058
|
-
'curl https://example.test/openssl -u reader:fixture-password',
|
|
1059
|
-
'curl -o openssl -u reader:fixture-password https://example.test',
|
|
1060
|
-
'http example.test/sshpass --auth reader:fixture-password',
|
|
1061
|
-
'curl --output sshpass --proxy-user reader:fixture-password https://example.test',
|
|
1062
|
-
'sshpass -p fixture-first curl -u reader:fixture-password https://example.test',
|
|
1063
|
-
])('keeps client identity across argument names: %s', (command) => {
|
|
1064
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1065
|
-
});
|
|
1066
|
-
it.each(['url', 'preproxy', 'socks4', 'socks4a', 'socks5', 'socks5-hostname'])('redacts userinfo in the %s config field and flag', (key) => {
|
|
1067
|
-
for (const command of [
|
|
1068
|
-
`echo '${key} = reader:fixture-password@proxy.test:3128' | curl -K -`,
|
|
1069
|
-
`curl --${key} reader:fixture-password@proxy.test:3128 https://example.test`,
|
|
1070
|
-
]) {
|
|
1071
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1072
|
-
expect(sanitizeCommand(command)).toContain('proxy.test:3128');
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
1075
|
-
it.each([
|
|
1076
|
-
`echo 'user = reader:fixture-password' | bash -c 'curl -K - https://example.test'`,
|
|
1077
|
-
`printf 'cookie = "%s"' SID=fixture-password | env -S 'curl -K - https://example.test'`,
|
|
1078
|
-
`echo 'user = reader:fixture-password' | sh -c "bash -c 'curl -K - https://example.test'"`,
|
|
1079
|
-
])('detects nested config consumers from outer producers: %s', (command) => {
|
|
1080
|
-
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
1081
|
-
});
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
describe('credential-tool names outside command position', () => {
|
|
1085
|
-
// A path argument whose basename spells a credential tool names a file, not an
|
|
1086
|
-
// invocation: the words after it keep their meaning.
|
|
1087
|
-
it.each([
|
|
1088
|
-
'cat ./bin/curl -u reader:visible',
|
|
1089
|
-
'ls ./bin/curl image:tag@sha256:abc123',
|
|
1090
|
-
'cat scripts/http image:tag@sha256:abc123',
|
|
1091
|
-
'chmod +x ./bin/openssl -k visible-file',
|
|
1092
|
-
'grep -r key /etc/openssl -K visible-file',
|
|
1093
|
-
'cat bin/mysql -pvisible',
|
|
1094
|
-
'cat tools/redis-cli -a visible-file',
|
|
1095
|
-
'cat bin/sshpass -p visible-file',
|
|
1096
|
-
'sudo cat ./bin/curl -u reader:visible',
|
|
1097
|
-
'sudo -u deploy env MODE=1 cat ./bin/curl -u reader:visible',
|
|
1098
|
-
'sudo -E cat ./bin/curl -u reader:visible',
|
|
1099
|
-
'sudo --preserve-env=HOME cat ./bin/curl -u reader:visible',
|
|
1100
|
-
'env -i cat ./bin/curl -u reader:visible',
|
|
1101
|
-
'xargs -0 cat ./bin/curl -u reader:visible',
|
|
1102
|
-
'time -p cat ./bin/curl -u reader:visible',
|
|
1103
|
-
'/usr/bin/cat ./bin/curl -u reader:visible',
|
|
1104
|
-
])('keeps the arguments of a program that never runs them: %s', (command) => {
|
|
1105
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
1106
|
-
});
|
|
1107
|
-
it('does not read curl configuration from a file argument named curl', () => {
|
|
1108
|
-
const command = 'cat bin/curl -K cfg; echo user = reader:visible';
|
|
1109
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
1110
|
-
});
|
|
1111
|
-
it.each(['echo', 'printf'])('does not treat a file argument named %s as a configuration producer', (producer) => {
|
|
1112
|
-
const command = `curl -K cfg https://example.test; ls bin/${producer} -u reader:visible`;
|
|
1113
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
1114
|
-
});
|
|
1115
|
-
const familyInvocations = [
|
|
1116
|
-
['curl', 'curl -u reader:fixture-password https://example.test'],
|
|
1117
|
-
...['http', 'https', 'httpie'].map((tool) => [tool, `${tool} -a reader:fixture-password example.test`]),
|
|
1118
|
-
['openssl', 'openssl enc -aes-256-cbc -pass pass:fixture-password -in public-file'],
|
|
1119
|
-
['sshpass', 'sshpass -p fixture-password ssh deploy@host'],
|
|
1120
|
-
...['redis-cli', 'valkey-cli'].map((tool) => [tool, `${tool} -a fixture-password get k`]),
|
|
1121
|
-
...[
|
|
1122
|
-
'mysql',
|
|
1123
|
-
'mariadb',
|
|
1124
|
-
'mysqldump',
|
|
1125
|
-
'mysqladmin',
|
|
1126
|
-
'mariadb-dump',
|
|
1127
|
-
'mariadb-admin',
|
|
1128
|
-
].map((tool) => [tool, `${tool} -pfixture-password db`]),
|
|
1129
|
-
];
|
|
1130
|
-
it.each(familyInvocations)('still redacts a real /usr/bin/%s invocation', (_, command) => {
|
|
1131
|
-
for (const prefix of [
|
|
1132
|
-
'/usr/bin/',
|
|
1133
|
-
'cat notes.txt && /usr/bin/',
|
|
1134
|
-
'sudo /usr/bin/',
|
|
1135
|
-
]) {
|
|
1136
|
-
const output = sanitizeCommand(`${prefix}${command}`);
|
|
1137
|
-
expect(output).not.toContain('fixture-password');
|
|
1138
|
-
expect(output).toContain('[redacted]');
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
it.each([
|
|
1142
|
-
'watch -n 5 curl -u reader:fixture-password https://example.test',
|
|
1143
|
-
String.raw `find . -exec /usr/bin/curl -u reader:fixture-password https://example.test \;`,
|
|
1144
|
-
'ssh deploy@host /usr/bin/curl -u reader:fixture-password https://example.test',
|
|
1145
|
-
'xargs /usr/bin/curl -u reader:fixture-password https://example.test',
|
|
1146
|
-
'echo curl -u reader:fixture-password',
|
|
1147
|
-
// A wrapper option the table does not model may take the next word as its
|
|
1148
|
-
// value, so that word is not trusted as the program the wrapper runs.
|
|
1149
|
-
'xargs --process-slot-var cat curl -u reader:fixture-password https://example.test',
|
|
1150
|
-
'sudo -U cat curl -u reader:fixture-password https://example.test',
|
|
1151
|
-
'env -P ls curl -u reader:fixture-password https://example.test',
|
|
1152
|
-
'sudo -U env cat curl -u reader:fixture-password https://example.test',
|
|
1153
|
-
// A local script named like a listed program may run anything.
|
|
1154
|
-
'./tools/diff curl -u reader:fixture-password https://example.test',
|
|
1155
|
-
'/opt/bin/cat curl -u reader:fixture-password https://example.test',
|
|
1156
|
-
'cat x; sudo -u root ./scripts/cat curl -u reader:fixture-password https://example.test',
|
|
1157
|
-
// A redirection operand names a file, not the command that runs.
|
|
1158
|
-
'>file curl -u reader:fixture-password https://example.test',
|
|
1159
|
-
'< cat curl -u reader:fixture-password https://example.test',
|
|
1160
|
-
'>|ls /usr/bin/curl -u reader:fixture-password https://example.test',
|
|
1161
|
-
'>file echo user = reader:fixture-password | curl -K - https://example.test',
|
|
1162
|
-
// A backtick substitution runs its own command under any outer one.
|
|
1163
|
-
'cat `/usr/bin/curl -u reader:fixture-password https://example.test`',
|
|
1164
|
-
])('still redacts a tool dispatched by a program it does not model: %s', (command) => {
|
|
1165
|
-
expect(sanitizeCommand(command)).not.toContain('fixture-password');
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
describe('backtick command substitutions', () => {
|
|
1169
|
-
// One command per credential-tool family, plus a URI and an assignment.
|
|
1170
|
-
const bodies = [
|
|
1171
|
-
'curl -u reader:fixture-visible https://example.test',
|
|
1172
|
-
'http -a reader:fixture-visible https://example.test',
|
|
1173
|
-
'openssl enc -aes-256-cbc -pass pass:fixture-visible -in notes.txt',
|
|
1174
|
-
'sshpass -p fixture-visible ssh deploy@host',
|
|
1175
|
-
'redis-cli -a fixture-visible ping',
|
|
1176
|
-
'valkey-cli --pass fixture-visible ping',
|
|
1177
|
-
'mysql -pvisible app',
|
|
1178
|
-
'curl https://reader:fixture-visible@example.test',
|
|
1179
|
-
'redis-cli -u redis://reader:fixture-visible@cache.test ping',
|
|
1180
|
-
'TOKEN=fixture-visible ./deploy',
|
|
1181
|
-
'tool --password fixture-visible',
|
|
1182
|
-
];
|
|
1183
|
-
const forms = [
|
|
1184
|
-
['after echo', (body) => `echo \`${body}\``],
|
|
1185
|
-
['after cat', (body) => `cat \`${body}\``],
|
|
1186
|
-
['as the command', (body) => `\`${body}\``],
|
|
1187
|
-
['as an assignment value', (body) => `out=\`${body}\``],
|
|
1188
|
-
['inside double quotes', (body) => `echo "\`${body}\`"`],
|
|
1189
|
-
['inside single quotes', (body) => `echo '\`${body}\`'`],
|
|
1190
|
-
['escaped', (body) => `echo \\\`${body}\\\``],
|
|
1191
|
-
['nested', (body) => `echo \`echo \\\`${body}\\\`\``],
|
|
1192
|
-
['after a separator inside', (body) => `echo \`cd /tmp; ${body}\``],
|
|
1193
|
-
['after a pipe inside', (body) => `echo \`true | ${body}\``],
|
|
1194
|
-
['inside $(…)', (body) => `echo $(echo \`${body}\`)`],
|
|
1195
|
-
['inside <(…)', (body) => `diff <(echo \`${body}\`) notes.txt`],
|
|
1196
|
-
['unterminated', (body) => `echo \`${body}`],
|
|
1197
|
-
];
|
|
1198
|
-
it.each(forms.flatMap(([form, wrap]) => bodies.map((body) => [form, wrap(body)])))('redacts a credential in a backtick substitution %s: %s', (_, command) => {
|
|
1199
|
-
const output = sanitizeCommand(command);
|
|
1200
|
-
expect(output).not.toContain('visible');
|
|
1201
|
-
expect(output).toContain('[redacted]');
|
|
1202
|
-
});
|
|
1203
|
-
it('keeps the rest of the substitution, as for $(…)', () => {
|
|
1204
|
-
expect(sanitizeCommand('echo `curl -u reader:fixture-visible https://example.test`')).toBe('echo `curl -u [redacted] https://example.test`');
|
|
1205
|
-
expect(sanitizeCommand('echo `mysql -pvisible app` done')).toBe('echo `mysql -p[redacted] app` done');
|
|
1206
|
-
expect(sanitizeCommand('echo \\`curl -u reader:fixture-visible https://example.test\\`')).toBe('echo \\`curl -u [redacted] https://example.test\\`');
|
|
1207
|
-
expect(sanitizeCommand('echo `echo \\`curl -u reader:fixture-visible https://example.test\\``')).toBe('echo `echo \\`curl -u [redacted] https://example.test\\``');
|
|
1208
|
-
// Escapes the body carried come back as they were written.
|
|
1209
|
-
expect(sanitizeCommand("echo `printf 'a\\nb'; echo \\$HOME; curl -u reader:fixture-visible https://example.test`")).toBe("echo `printf 'a\\nb'; echo \\$HOME; curl -u [redacted] https://example.test`");
|
|
1210
|
-
});
|
|
1211
|
-
it('redacts everything after an unterminated backtick', () => {
|
|
1212
|
-
expect(sanitizeCommand('echo `curl -u reader:fixture-visible https://example.test')).toBe('echo `[redacted]');
|
|
1213
|
-
expect(sanitizeCommand('echo `date` `printf fixture-visible')).toBe('echo `date` `[redacted]');
|
|
1214
|
-
});
|
|
1215
|
-
it('links curl config across the substitution boundary', () => {
|
|
1216
|
-
for (const command of [
|
|
1217
|
-
'echo `echo user = reader:fixture-visible | curl -K - https://example.test`',
|
|
1218
|
-
'`echo user = reader:fixture-visible` | curl -K - https://example.test',
|
|
1219
|
-
'echo user = reader:fixture-visible | `curl -K - https://example.test`',
|
|
1220
|
-
]) {
|
|
1221
|
-
const output = sanitizeCommand(command);
|
|
1222
|
-
expect(output).not.toContain('visible');
|
|
1223
|
-
}
|
|
1224
|
-
});
|
|
1225
|
-
// Redacting the body must not hide which outer assignment it reads.
|
|
1226
|
-
it.each([
|
|
1227
|
-
`export VALUE=fixture-visible; echo \`sh -c 'curl --variable %VALUE --expand-header "Authorization: {{VALUE}}" https://example.test'\``,
|
|
1228
|
-
`export VALUE=fixture-visible; echo \`curl --variable %VALUE --expand-header "Authorization: {{VALUE}}" https://example.test\``,
|
|
1229
|
-
])('redacts an outer assignment a substitution expands: %s', (command) => {
|
|
1230
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1231
|
-
});
|
|
1232
|
-
// A substitution inside a word must not split the word from its reference.
|
|
1233
|
-
it.each([
|
|
1234
|
-
'export VALUE=:fixture-visible; sh -c "curl --variable %VALUE --expand-user `whoami`{{VALUE}} https://example.test"',
|
|
1235
|
-
'export VALUE=:fixture-visible; curl --variable %VALUE --expand-user `whoami`{{VALUE}} https://example.test',
|
|
1236
|
-
])('redacts an assignment a word expands after a substitution: %s', (command) => {
|
|
1237
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1238
|
-
});
|
|
1239
|
-
// Rewriting part of a body must not strand a header's value.
|
|
1240
|
-
it.each([
|
|
1241
|
-
'echo \'`echo "`curl -H "Authorization: fixture-visible" https://example.test`"`\'',
|
|
1242
|
-
'echo `echo "`curl -H "Authorization: fixture-visible" https://example.test`"`',
|
|
1243
|
-
'TOKEN=`date`; echo `echo "`curl -H "Authorization: fixture-visible" https://example.test`"`',
|
|
1244
|
-
])('redacts a header split across substitution bodies: %s', (command) => {
|
|
1245
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1246
|
-
});
|
|
1247
|
-
// A substitution elsewhere must not hide which curl variable a header reads.
|
|
1248
|
-
it.each([
|
|
1249
|
-
'curl --variable value=fixture-visible --expand-header "Authorization: {{value}}" https://example.test; echo `date`',
|
|
1250
|
-
'curl --variable value=fixture-visible --expand-url "https://u:{{value}}@example.test" `date`',
|
|
1251
|
-
'curl --variable value=fixture-visible --expand-url "https://example.test/?token={{value}}" `date`',
|
|
1252
|
-
])('redacts an inline curl variable beside a substitution: %s', (command) => {
|
|
1253
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1254
|
-
});
|
|
1255
|
-
// A substitution elsewhere must not cut a quoted value short in a script.
|
|
1256
|
-
it.each([
|
|
1257
|
-
'sh -c "tool --password \\"a fixture-visible\\""; echo `date`',
|
|
1258
|
-
'sh -c "curl -H \\"Authorization: Bearer a fixture-visible\\" https://example.test"; echo `date`',
|
|
1259
|
-
])('redacts a spaced value in an escaped script beside a substitution: %s', (command) => {
|
|
1260
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1261
|
-
});
|
|
1262
|
-
// Inside double quotes a backtick body also escapes `"`.
|
|
1263
|
-
it.each([
|
|
1264
|
-
'sh -c "echo `tool --password \\"a fixture-visible\\"`"',
|
|
1265
|
-
'echo "`tool --password \\"a fixture-visible\\"`"',
|
|
1266
|
-
])('redacts a spaced value in a double-quoted substitution: %s', (command) => {
|
|
1267
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1268
|
-
});
|
|
1269
|
-
// Bash ends a body at the first unescaped backtick: single quotes inside it
|
|
1270
|
-
// hide none, and an escaped one is part of the body.
|
|
1271
|
-
it.each([
|
|
1272
|
-
'echo "`sh -c \'echo \\`tool --password \\"a fixture-visible\\"\\`\'`"',
|
|
1273
|
-
'echo `sh -c \'echo \\`tool --password "a fixture-visible"\\`\'`',
|
|
1274
|
-
'echo "`sh -c \'echo \\`curl -u reader:fixture-visible https://example.test\\`\'`"',
|
|
1275
|
-
])('redacts a body whose escaped backtick sits in single quotes: %s', (command) => {
|
|
1276
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1277
|
-
});
|
|
1278
|
-
// The word a header value continues into is still the value.
|
|
1279
|
-
it.each([
|
|
1280
|
-
'curl -H "Cookie: sid=fixture-visible"more-visible https://example.test; echo `date`',
|
|
1281
|
-
'curl -H "Cookie: sid=fixture-visible"more-visible `date`',
|
|
1282
|
-
'echo "Cookie: sid=fixture-visible"more-visible `date`',
|
|
1283
|
-
// Redacting the first header early must not strand the second.
|
|
1284
|
-
'x= "Cookie: sid=fixture-visible""Authorization: {{VALUE}}" PASSWORD="`id`" { "Cookie: sid=fixture-visible"',
|
|
1285
|
-
])('redacts a header value that runs past its quote beside a substitution: %s', (command) => {
|
|
1286
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1287
|
-
});
|
|
1288
|
-
// Quoting the parser cannot follow leaves a backtick on the tool's word.
|
|
1289
|
-
it.each([
|
|
1290
|
-
"echo '`curl -u reader:fixture-visible https://example.test",
|
|
1291
|
-
'echo ``curl -u reader:fixture-visible https://example.test',
|
|
1292
|
-
"echo `echo '`' `curl -u reader:fixture-visible https://example.test`",
|
|
1293
|
-
])('redacts a tool whose word carries a backtick: %s', (command) => {
|
|
1294
|
-
expect(sanitizeCommand(command)).not.toContain('visible');
|
|
1295
|
-
});
|
|
1296
|
-
it.each([
|
|
1297
|
-
'echo `date`',
|
|
1298
|
-
'git checkout `git rev-parse HEAD`',
|
|
1299
|
-
'cat `which curl`',
|
|
1300
|
-
'ls `pwd`/notes',
|
|
1301
|
-
'echo "`whoami`"',
|
|
1302
|
-
// A quoted word with a space inside a body is not an unterminated backtick.
|
|
1303
|
-
'x=`date "+%Y %m"`',
|
|
1304
|
-
'echo `echo "a b"`',
|
|
1305
|
-
"echo `echo 'a b'`",
|
|
1306
|
-
'echo "`date "+%Y %m"`"',
|
|
1307
|
-
String.raw `echo "a \` b"`,
|
|
1308
|
-
String.raw `echo \`date\``,
|
|
1309
|
-
])('leaves a substitution with no credential unchanged: %s', (command) => {
|
|
1310
|
-
expect(sanitizeCommand(command)).toBe(command);
|
|
3
|
+
describe('executable-only command evidence', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['git diff -- apps/private.txt', 'git'],
|
|
6
|
+
['bun run test && curl --token fixture-private', 'bun'],
|
|
7
|
+
['curl -u fixture-user:fixture-password https://private.test', 'curl'],
|
|
8
|
+
["sh -c 'curl --token fixture-private'", 'sh'],
|
|
9
|
+
['sudo -u fixture-private git status', 'sudo'],
|
|
10
|
+
['env PASSWORD=fixture-private node script.js', 'env'],
|
|
11
|
+
[' \n\tpython3 -c "print(123)"', 'python3'],
|
|
12
|
+
['cat <<EOF\nfixture-private\nEOF', 'cat'],
|
|
13
|
+
['mysql Cookie: sid=fixture-private" \t\\--token\t"--secret"--a', 'mysql'],
|
|
14
|
+
["'--token' 'fixture-private Cookie: sid=fixture-private'", '[command withheld]'],
|
|
15
|
+
[
|
|
16
|
+
"'--passwd' $(fixture-private )Authorization: fixture-private",
|
|
17
|
+
'[command withheld]',
|
|
18
|
+
],
|
|
19
|
+
])('omits all arguments and nested source from %s', (source, expected) => {
|
|
20
|
+
expect(sanitizeCommand(source)).toBe(expected);
|
|
21
|
+
expect(sanitizeCommand(source)).not.toContain('fixture-private');
|
|
22
|
+
});
|
|
23
|
+
it.each([
|
|
24
|
+
'',
|
|
25
|
+
'fixture-private',
|
|
26
|
+
'fixture-private --token value',
|
|
27
|
+
'PASSWORD=fixture-private git status',
|
|
28
|
+
'/private/path/git status',
|
|
29
|
+
"'git' status",
|
|
30
|
+
'g\\it status',
|
|
31
|
+
'$(fixture-private)',
|
|
32
|
+
'`fixture-private`',
|
|
33
|
+
'gitfixture-private',
|
|
34
|
+
'git=fixture-private',
|
|
35
|
+
'git.exe fixture-private',
|
|
36
|
+
'"git fixture-private',
|
|
37
|
+
'git\\ fixture-private',
|
|
38
|
+
'# fixture-private\ngit status',
|
|
39
|
+
' '.repeat(128) + 'git status',
|
|
40
|
+
'g'.repeat(100_000),
|
|
41
|
+
])('withholds unsupported source without echoing it: %s', (source) => {
|
|
42
|
+
expect(sanitizeCommand(source)).toBe('[command withheld]');
|
|
43
|
+
});
|
|
44
|
+
it('does not inspect or preserve a large argument body', () => {
|
|
45
|
+
expect(sanitizeCommand('curl ' + 'fixture-private'.repeat(100_000))).toBe('curl');
|
|
1311
46
|
});
|
|
1312
47
|
});
|
|
1313
48
|
//# sourceMappingURL=sanitize-command.test.js.map
|