Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/mongodb-redact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"typescript": "^5.0.4"
},
"dependencies": {
"regexp.escape": "^2.0.1"
"regexp.escape": "^2.0.1",
"mongodb-connection-string-url": "^3.0.1"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the way this is now at 7.0.0, I'm keeping at same version as mongosh to minimize side effects for now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you can do, if you want to, is to specify

Suggested change
"mongodb-connection-string-url": "^3.0.1"
"mongodb-connection-string-url": "^3.0.1 || ^7.0.0"

and give control to the user of this package, since it's pretty reasonable to assume that both of these would work just fine

}
}
83 changes: 76 additions & 7 deletions packages/mongodb-redact/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,82 @@ describe('mongodb-redact', function () {
expect(res).to.equal('<url>');
});

it('should redact MongoDB connection URIs', function () {
let res = redact(
'mongodb://db1.example.net,db2.example.net:2500/?replicaSet=test&connectTimeoutMS=300000',
);
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://localhost,localhost:27018,localhost:27019');
expect(res).to.equal('<mongodb uri>');
describe('MongoDB connection strings', function () {
it('should redact MongoDB connection URIs', function () {
let res = redact(
'mongodb://db1.example.net,db2.example.net:2500/?replicaSet=test&connectTimeoutMS=300000',
);
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://localhost,localhost:27018,localhost:27019');
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB URIs with credentials', function () {
let res = redact('mongodb://user:password@localhost:27017/admin');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://admin:[email protected]/mydb');
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB URIs with special characters in usernames and passwords', function () {
let res = redact('mongodb://user:p%40ss!word@localhost:27017/');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://ad!min:te%st#[email protected]:27017/');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://!user:my%20pass@localhost/mydb');
expect(res).to.equal('<mongodb uri>');
res = redact(
'mongodb://user:p&ssw!rd#[email protected]:27017/db?authSource=admin',
);
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB SRV URIs', function () {
let res = redact(
'mongodb+srv://user:[email protected]/test',
);
expect(res).to.equal('<mongodb uri>');
res = redact(
'mongodb+srv://admin:[email protected]/mydb?retryWrites=true',
);
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB URIs with query parameters', function () {
let res = redact(
'mongodb://localhost:27017/mydb?ssl=true&replicaSet=rs0',
);
expect(res).to.equal('<mongodb uri>');
res = redact(
'mongodb://user:[email protected]/db?authSource=admin&readPreference=primary',
);
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB URIs with replica sets', function () {
let res = redact(
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet',
);
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://user:pass@host1,host2,host3/db?replicaSet=rs0');
expect(res).to.equal('<mongodb uri>');
});

it('should redact MongoDB URIs with IP addresses', function () {
let res = redact('mongodb://192.168.1.100:27017/mydb');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://user:[email protected]:27017/admin');
expect(res).to.equal('<mongodb uri>');
});

it('should redact simple MongoDB URIs', function () {
let res = redact('mongodb://localhost');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://localhost:27017');
expect(res).to.equal('<mongodb uri>');
res = redact('mongodb://localhost/mydb');
expect(res).to.equal('<mongodb uri>');
});
});

it('should redact general linux/unix user paths', function () {
Expand Down
2 changes: 2 additions & 0 deletions packages/mongodb-redact/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ export function redact<T>(
return message;
}

export { redactUriCredentials } from './redact-uri-credentials';

export default redact;
export type { Secret } from './secrets';
173 changes: 173 additions & 0 deletions packages/mongodb-redact/src/redact-uri-credentials.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { expect } from 'chai';
import { redactUriCredentials } from './redact-uri-credentials';
import redact from '.';

describe('redactUriCredentials', function () {
const testCases: Array<{
description: string;
input: string;
expected: string;
}> = [
{
description: 'should redact username and password',
input: 'mongodb://user:password@localhost:27017/admin',
expected: 'mongodb://<credentials>@localhost:27017/admin',
},
{
description: 'should redact only username when no password',
input: 'mongodb://user@localhost:27017/admin',
expected: 'mongodb://<credentials>@localhost:27017/admin',
},
{
description: 'should redact credentials in SRV URIs',
input: 'mongodb+srv://admin:[email protected]/test',
expected: 'mongodb+srv://<credentials>@cluster0.example.com/test',
},
{
description: 'should redact passwords with ! character',
input: 'mongodb://user:p@ss!word@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should redact passwords with # character',
input: 'mongodb://admin:test#[email protected]:27017/',
expected: 'mongodb://<credentials>@db.example.com:27017/',
},
{
description: 'should redact passwords with $ character',
input: 'mongodb://user:price$100@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should redact passwords with % character',
input: 'mongodb://user:test%pass@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should redact passwords with & character',
input: 'mongodb://user:rock&roll@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should redact URL-encoded passwords',
input: 'mongodb://user:my%20password@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description:
'should redact complex passwords with multiple special characters',
input: 'mongodb://user:p&ssw!rd#[email protected]:27017/db?authSource=admin',
expected: 'mongodb://<credentials>@host.com:27017/db?authSource=admin',
},
{
description: 'should redact usernames with special characters',
input: 'mongodb://us!er:password@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should return URI unchanged when no credentials',
input: 'mongodb://localhost:27017/admin',
expected: 'mongodb://localhost:27017/admin',
},
{
description: 'should handle simple localhost URI',
input: 'mongodb://localhost',
expected: 'mongodb://localhost/',
},
{
description: 'should handle URI with database',
input: 'mongodb://localhost/mydb',
expected: 'mongodb://localhost/mydb',
},
{
description: 'should handle URI with query parameters',
input: 'mongodb://localhost:27017/mydb?ssl=true&replicaSet=rs0',
expected: 'mongodb://localhost:27017/mydb?ssl=true&replicaSet=rs0',
},
// URIs with replica sets
{
description: 'should redact credentials in replica set URIs',
input:
'mongodb://user:pass@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0',
expected:
'mongodb://<credentials>@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0',
},
{
description: 'should handle replica set URIs without credentials',
input:
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet',
expected:
'mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=myReplSet',
},
// URIs with IP addresses
{
description: 'should redact credentials with IP address host',
input: 'mongodb://user:[email protected]:27017/mydb',
expected: 'mongodb://<credentials>@192.168.1.100:27017/mydb',
},
{
description: 'should handle IP address URIs without credentials',
input: 'mongodb://10.0.0.5:27017/admin',
expected: 'mongodb://10.0.0.5:27017/admin',
},
// SRV URIs
{
description: 'should handle SRV URIs without credentials',
input: 'mongodb+srv://cluster0.example.com/test',
expected: 'mongodb+srv://cluster0.example.com/test',
},
// URIs with query parameters
{
description: 'should redact credentials and preserve query parameters',
input:
'mongodb://user:[email protected]/db?authSource=admin&readPreference=primary',
expected:
'mongodb://<credentials>@host.com/db?authSource=admin&readPreference=primary',
},
{
description: 'should handle URIs with SSL options',
input:
'mongodb://admin:secret@localhost:27017/mydb?ssl=true&tlsAllowInvalidCertificates=true',
expected:
'mongodb://<credentials>@localhost:27017/mydb?ssl=true&tlsAllowInvalidCertificates=true',
},
{
description: 'should redact credentials in SRV URIs with query params',
input:
'mongodb+srv://admin:[email protected]/mydb?retryWrites=true',
expected:
'mongodb+srv://<credentials>@mycluster.mongodb.net/mydb?retryWrites=true',
},
// Edge cases
{
description: 'should handle empty password',
input: 'mongodb://user:@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description:
'should handle password with only special characters (URL-encoded)',
input: 'mongodb://user:%21%40%23%24%25@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should handle very long passwords',
input: `mongodb://user:${'a'.repeat(100)}@localhost:27017/`,
expected: 'mongodb://<credentials>@localhost:27017/',
},
{
description: 'should handle international characters in password',
input: 'mongodb://user:пароль@localhost:27017/',
expected: 'mongodb://<credentials>@localhost:27017/',
},
];

testCases.forEach(({ description, input, expected }) => {
it(description, function () {
const result = redactUriCredentials(input);
expect(result).to.equal(expected);

expect(redact(input)).to.equal('<mongodb uri>');
});
});
});
5 changes: 5 additions & 0 deletions packages/mongodb-redact/src/redact-uri-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redactConnectionString } from 'mongodb-connection-string-url';

export function redactUriCredentials(uri: string): string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this redactConnectionString to keep identifier continuity? Or is there a specific reason that this should be named redactUriCredentials? Being able to grep for something and find all instances of that thing is generally quite helpful

(in this particular case, "URI credentials" might also be a bit misleading because in the context of URIs, credentials refer to username and password specifically, but we also filter out known credentials in the connection string like AWS session tokens or private key file passwords because we're aware of connection string semantics here)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was meant to be redactURICredentials solely because mongosh history's function this is copying was named that, so not really a deep reason.
redactConnectionString sounds better to me

return redactConnectionString(uri);
}
9 changes: 3 additions & 6 deletions packages/mongodb-redact/src/regexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const regexes = [
'$1<email>$6',
],

// MongoDB connection strings
[/mongodb(?:\+srv)?:\/\/\S+/gim, '<mongodb uri>'],

// IP addresses
[
/((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])/gm,
Expand All @@ -39,12 +42,6 @@ export const regexes = [
'<url>',
],

// MongoDB connection strings
[
/(mongodb:\/\/)(www\.)?[-a-zA-Z0-9@:%._+~#=,]{2,256}(\.[a-z]{2,6})?\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/gim,
'<mongodb uri>',
],

// Compass Schema URL fragments
[/#schema\/\w+\.\w+/, '#schema/<namespace>'],
] as const;
51 changes: 51 additions & 0 deletions packages/mongodb-redact/src/secrets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,55 @@ describe('dictionary-based secret redaction', function () {
usr: '<user>',
});
});

describe('special characters in passwords', function () {
it('redacts passwords at start, end, or entire string', function () {
expect(
redact('!start is pwd', [{ value: '!start', kind: 'password' }]),
).to.equal('<password> is pwd');

expect(
redact('pwd is end!', [{ value: 'end!', kind: 'password' }]),
).to.equal('pwd is <password>');

expect(
redact('The password is !@#$%', [{ value: '!@#$%', kind: 'password' }]),
).to.equal('The password is <password>');
});

it('redacts a special-character only connection string', function () {
const secret = '!#!!';
const content = 'Connection string: mongodb://!!!#:!#!!@localhost:27017/';

const redacted = redact(content, [{ value: secret, kind: 'password' }]);

expect(redacted).to.equal('Connection string: <mongodb uri>');
});

for (const { char, password } of [
{ char: '.', password: 'test.pass' },
{ char: '*', password: 'test*pass' },
{ char: '+', password: 'test+pass' },
{ char: '?', password: 'test?pass' },
{ char: '[', password: 'test[123]' },
{ char: '(', password: 'test(abc)' },
{ char: '|', password: 'test|pass' },
{ char: '\\', password: 'test\\pass' },
{ char: '^', password: '^test123' },
{ char: '$', password: 'test$123' },
{ char: '@', password: 'user@123' },
{ char: '#', password: 'pass#word' },
{ char: '%', password: 'test%20' },
{ char: '&', password: 'rock&roll' },
{ char: 'լավ', password: 'լավ' },
]) {
it(`redacts passwords with ${char}`, function () {
const content = `pwd: ${password} end`;
const redacted = redact(content, [
{ value: password, kind: 'password' },
]);
expect(redacted).to.equal('pwd: <password> end');
});
}
});
});
5 changes: 4 additions & 1 deletion packages/mongodb-redact/src/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export function redactSecretsOnString<T extends string>(
);
}

const regex = new RegExp(`\\b${escape(value)}\\b`, 'g');
// Escape the value for use in regex and use negative lookahead/lookbehind
// to match secrets not surrounded by word characters
const escapedValue = escape(value);
const regex = new RegExp(`(?<!\\w)${escapedValue}(?!\\w)`, 'g');
result = result.replace(regex, `<${kind}>`) as T;
}

Expand Down
Loading
Loading