Skip to content

Commit adcb741

Browse files
authored
feat(mongodb-react): add shouldRedactCommand, redactUriCredentials MONGOSH-2991 (#599)
1 parent c7ee07d commit adcb741

File tree

8 files changed

+245
-6
lines changed

8 files changed

+245
-6
lines changed

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/devtools-connect/src/log-hook.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function hookLogger(
2828
emitter: ConnectLogEmitter,
2929
log: MongoLogWriter,
3030
contextPrefix: string,
31-
redactURICredentials: (uri: string) => string,
31+
redactConnectionString: (uri: string) => string,
3232
): void {
3333
oidcHookLogger(emitter, log, contextPrefix);
3434
proxyHookLogger(emitter, log, contextPrefix);
@@ -44,7 +44,7 @@ export function hookLogger(
4444
'Initiating connection attempt',
4545
{
4646
...ev,
47-
uri: redactURICredentials(ev.uri),
47+
uri: redactConnectionString(ev.uri),
4848
},
4949
);
5050
},
@@ -119,7 +119,7 @@ export function hookLogger(
119119
`${contextPrefix}-connect`,
120120
'Resolving SRV record failed',
121121
{
122-
from: redactURICredentials(ev.from),
122+
from: redactConnectionString(ev.from),
123123
error: ev.error?.message,
124124
duringLoad: ev.duringLoad,
125125
resolutionDetails: ev.resolutionDetails,
@@ -138,8 +138,8 @@ export function hookLogger(
138138
`${contextPrefix}-connect`,
139139
'Resolving SRV record succeeded',
140140
{
141-
from: redactURICredentials(ev.from),
142-
to: redactURICredentials(ev.to),
141+
from: redactConnectionString(ev.from),
142+
to: redactConnectionString(ev.to),
143143
resolutionDetails: ev.resolutionDetails,
144144
durationMs: ev.durationMs,
145145
},

packages/mongodb-redact/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"typescript": "^5.0.4"
7272
},
7373
"dependencies": {
74-
"regexp.escape": "^2.0.1"
74+
"regexp.escape": "^2.0.1",
75+
"mongodb-connection-string-url": "^3.0.1 || ^7.0.0"
7576
}
7677
}

packages/mongodb-redact/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,8 @@ export function redact<T>(
4646
return message;
4747
}
4848

49+
export { shouldRedactCommand } from './should-redact-command';
50+
export { redactConnectionString } from './redact-connection-string';
51+
4952
export default redact;
5053
export type { Secret } from './secrets';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { expect } from 'chai';
2+
import { redactConnectionString } from './redact-connection-string';
3+
import redact from '.';
4+
5+
describe('redactConnectionString', function () {
6+
const testCases: Array<{
7+
description: string;
8+
input: string;
9+
expected: string;
10+
}> = [
11+
{
12+
description: 'should redact username and password',
13+
input: 'mongodb://user:password@localhost:27017/admin',
14+
expected: 'mongodb://<credentials>@localhost:27017/admin',
15+
},
16+
{
17+
description: 'should redact only username when no password',
18+
input: 'mongodb://user@localhost:27017/admin',
19+
expected: 'mongodb://<credentials>@localhost:27017/admin',
20+
},
21+
{
22+
description: 'should redact credentials in SRV URIs',
23+
input: 'mongodb+srv://admin:[email protected]/test',
24+
expected: 'mongodb+srv://<credentials>@cluster0.example.com/test',
25+
},
26+
{
27+
description: 'should redact passwords with ! character',
28+
input: 'mongodb://user:p@ss!word@localhost:27017/',
29+
expected: 'mongodb://<credentials>@localhost:27017/',
30+
},
31+
{
32+
description: 'should redact passwords with # character',
33+
input: 'mongodb://admin:test#[email protected]:27017/',
34+
expected: 'mongodb://<credentials>@db.example.com:27017/',
35+
},
36+
{
37+
description: 'should redact passwords with $ character',
38+
input: 'mongodb://user:price$100@localhost:27017/',
39+
expected: 'mongodb://<credentials>@localhost:27017/',
40+
},
41+
{
42+
description: 'should redact passwords with % character',
43+
input: 'mongodb://user:test%pass@localhost:27017/',
44+
expected: 'mongodb://<credentials>@localhost:27017/',
45+
},
46+
{
47+
description: 'should redact passwords with & character',
48+
input: 'mongodb://user:rock&roll@localhost:27017/',
49+
expected: 'mongodb://<credentials>@localhost:27017/',
50+
},
51+
{
52+
description: 'should redact URL-encoded passwords',
53+
input: 'mongodb://user:my%20password@localhost:27017/',
54+
expected: 'mongodb://<credentials>@localhost:27017/',
55+
},
56+
{
57+
description:
58+
'should redact complex passwords with multiple special characters',
59+
input: 'mongodb://user:p&ssw!rd#[email protected]:27017/db?authSource=admin',
60+
expected: 'mongodb://<credentials>@host.com:27017/db?authSource=admin',
61+
},
62+
{
63+
description: 'should redact usernames with special characters',
64+
input: 'mongodb://us!er:password@localhost:27017/',
65+
expected: 'mongodb://<credentials>@localhost:27017/',
66+
},
67+
{
68+
description: 'should return URI unchanged when no credentials',
69+
input: 'mongodb://localhost:27017/admin',
70+
expected: 'mongodb://localhost:27017/admin',
71+
},
72+
{
73+
description: 'should handle simple localhost URI',
74+
input: 'mongodb://localhost',
75+
expected: 'mongodb://localhost/',
76+
},
77+
{
78+
description: 'should handle URI with database',
79+
input: 'mongodb://localhost/mydb',
80+
expected: 'mongodb://localhost/mydb',
81+
},
82+
{
83+
description: 'should handle URI with query parameters',
84+
input: 'mongodb://localhost:27017/mydb?ssl=true&replicaSet=rs0',
85+
expected: 'mongodb://localhost:27017/mydb?ssl=true&replicaSet=rs0',
86+
},
87+
// URIs with replica sets
88+
{
89+
description: 'should redact credentials in replica set URIs',
90+
input:
91+
'mongodb://user:pass@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0',
92+
expected:
93+
'mongodb://<credentials>@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0',
94+
},
95+
{
96+
description: 'should handle replica set URIs without credentials',
97+
input:
98+
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet',
99+
expected:
100+
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet',
101+
},
102+
// URIs with IP addresses
103+
{
104+
description: 'should redact credentials with IP address host',
105+
input: 'mongodb://user:[email protected]:27017/mydb',
106+
expected: 'mongodb://<credentials>@192.168.1.100:27017/mydb',
107+
},
108+
{
109+
description: 'should handle IP address URIs without credentials',
110+
input: 'mongodb://10.0.0.5:27017/admin',
111+
expected: 'mongodb://10.0.0.5:27017/admin',
112+
},
113+
// SRV URIs
114+
{
115+
description: 'should handle SRV URIs without credentials',
116+
input: 'mongodb+srv://cluster0.example.com/test',
117+
expected: 'mongodb+srv://cluster0.example.com/test',
118+
},
119+
// URIs with query parameters
120+
{
121+
description: 'should redact credentials and preserve query parameters',
122+
input:
123+
'mongodb://user:[email protected]/db?authSource=admin&readPreference=primary',
124+
expected:
125+
'mongodb://<credentials>@host.com/db?authSource=admin&readPreference=primary',
126+
},
127+
{
128+
description: 'should handle URIs with SSL options',
129+
input:
130+
'mongodb://admin:secret@localhost:27017/mydb?ssl=true&tlsAllowInvalidCertificates=true',
131+
expected:
132+
'mongodb://<credentials>@localhost:27017/mydb?ssl=true&tlsAllowInvalidCertificates=true',
133+
},
134+
{
135+
description: 'should redact credentials in SRV URIs with query params',
136+
input:
137+
'mongodb+srv://admin:[email protected]/mydb?retryWrites=true',
138+
expected:
139+
'mongodb+srv://<credentials>@mycluster.mongodb.net/mydb?retryWrites=true',
140+
},
141+
// Edge cases
142+
{
143+
description: 'should handle empty password',
144+
input: 'mongodb://user:@localhost:27017/',
145+
expected: 'mongodb://<credentials>@localhost:27017/',
146+
},
147+
{
148+
description:
149+
'should handle password with only special characters (URL-encoded)',
150+
input: 'mongodb://user:%21%40%23%24%25@localhost:27017/',
151+
expected: 'mongodb://<credentials>@localhost:27017/',
152+
},
153+
{
154+
description: 'should handle very long passwords',
155+
input: `mongodb://user:${'a'.repeat(100)}@localhost:27017/`,
156+
expected: 'mongodb://<credentials>@localhost:27017/',
157+
},
158+
{
159+
description: 'should handle international characters in password',
160+
input: 'mongodb://user:пароль@localhost:27017/',
161+
expected: 'mongodb://<credentials>@localhost:27017/',
162+
},
163+
];
164+
165+
testCases.forEach(({ description, input, expected }) => {
166+
it(description, function () {
167+
const result = redactConnectionString(input);
168+
expect(result).to.equal(expected);
169+
170+
expect(redact(input)).to.equal('<mongodb uri>');
171+
});
172+
});
173+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redactConnectionString as redactConnectionStringImpl } from 'mongodb-connection-string-url';
2+
3+
export function redactConnectionString(uri: string): string {
4+
return redactConnectionStringImpl(uri);
5+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { expect } from 'chai';
2+
import { shouldRedactCommand } from '.';
3+
4+
describe('shouldRedactCommand', function () {
5+
for (const command of [
6+
'db.createUser({ user: "test" })',
7+
'db.auth("user", "pass")',
8+
'db.updateUser("user", { roles: [] })',
9+
'db.changeUserPassword("user", "newpass")',
10+
'db = connect("mongodb://localhost")',
11+
'new Mongo("mongodb://localhost")',
12+
]) {
13+
it(`returns true for ${command}`, function () {
14+
expect(shouldRedactCommand(command)).to.be.true;
15+
});
16+
}
17+
18+
for (const command of [
19+
'db.collection.find()',
20+
'db.collection.find({authentication: true})',
21+
'db.getUsers()',
22+
'show dbs',
23+
]) {
24+
it(`returns false for ${command}`, function () {
25+
expect(shouldRedactCommand(command)).to.be.false;
26+
});
27+
}
28+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Regex pattern for commands that contain sensitive information and should be
3+
* completely removed from history rather than redacted.
4+
*
5+
* These commands typically involve authentication or connection strings with credentials.
6+
*/
7+
const HIDDEN_COMMANDS = String.raw`\b(createUser|auth|updateUser|changeUserPassword|connect|Mongo)\b`;
8+
9+
/**
10+
* Checks if a mongosh command should be redacted because it often contains sensitive information like credentials.
11+
*
12+
* @param input - The command string to check
13+
* @returns true if the command should be hidden/redacted, false otherwise
14+
*
15+
* @example
16+
* ```typescript
17+
* shouldRedactCommand('db.createUser({user: "admin", pwd: "secret"})')
18+
* // Returns: true
19+
*
20+
* shouldRedactCommand('db.getUsers()')
21+
* // Returns: false
22+
* ```
23+
*/
24+
export function shouldRedactCommand(input: string): boolean {
25+
const hiddenCommands = new RegExp(HIDDEN_COMMANDS, 'g');
26+
return hiddenCommands.test(input);
27+
}

0 commit comments

Comments
 (0)