Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3b1d6e3

Browse files
authored
feat(redact): add a dictionary of secrets to redact MCP-29 (#569)
* feat(redact): add a dictionary of secrets to redact With this feature we support redacting known secrets from a dictionary. This is value for environments like the MCP Server where we create new users (user/password) or when we can infer secrets from CLI arguments (--user, --pasword). * chore: add support for arbitrarily deep object redacting * chore: refactor to reuse the iteration, instead of iterating twice * chore: fix linter warnings * chore: fix an issue when the constructor is not a function We don't want to return a truthy value if the ctor is a non function value (like an object). Plain objects either have a plain object as a prototype or a null prototype
1 parent 913ca6a commit 3b1d6e3

File tree

8 files changed

+260
-25
lines changed

8 files changed

+260
-25
lines changed

‎package-lock.json

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

‎packages/mongodb-redact/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@mongodb-js/tsconfig-devtools": "^1.0.3",
5959
"@types/chai": "^4.2.21",
6060
"@types/mocha": "^9.1.1",
61+
"@types/regexp.escape": "^2.0.0",
6162
"@types/sinon-chai": "^3.2.5",
6263
"chai": "^4.5.0",
6364
"depcheck": "^1.4.7",
@@ -68,5 +69,8 @@
6869
"prettier": "^3.5.3",
6970
"sinon": "^9.2.3",
7071
"typescript": "^5.0.4"
72+
},
73+
"dependencies": {
74+
"regexp.escape": "^2.0.1"
7175
}
7276
}

‎packages/mongodb-redact/src/index.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ const BIN_DATA = `db = db.getSiblingDB("__realm_sync")
2424
db.history.updateOne({"_id": ObjectId("63ed1d522d8573fa5c203660")}, {$set:{changeset:BinData(5, "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC")}})`;
2525

2626
describe('mongodb-redact', function () {
27+
describe('Secrets', function () {
28+
it('should redact provided secrets in strings', function () {
29+
const res = redact('foo@bar', [{ value: 'foo@bar', kind: 'password' }]);
30+
expect(res).to.equal('<password>');
31+
});
32+
33+
it('should redact provided secrets in objects', function () {
34+
const res = redact({ key: 'foo@bar' }, [
35+
{ value: 'foo@bar', kind: 'password' },
36+
]);
37+
expect(res).to.deep.equal({ key: '<password>' });
38+
});
39+
40+
it('should redact provided secrets in arrays', function () {
41+
const res = redact(['foo@bar'], [{ value: 'foo@bar', kind: 'password' }]);
42+
expect(res).to.deep.equal(['<password>']);
43+
});
44+
});
45+
2746
describe('Types', function () {
2847
it('should work with string types', function () {
2948
const res = redact('foo@bar.com');

‎packages/mongodb-redact/src/index.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,42 @@
11
import { regexes } from './regexes';
2+
import { isPlainObject } from './utils';
3+
import { redactSecretsOnString } from './secrets';
4+
import type { Secret } from './secrets';
25

3-
const plainObjectTag = Object.prototype.toString.call({});
4-
function isPlainObject(val: unknown): val is object {
5-
if (
6-
typeof val !== 'object' ||
7-
!val ||
8-
Object.prototype.toString.call(val) !== plainObjectTag
9-
) {
10-
return false;
11-
}
12-
const proto = Object.getPrototypeOf(val);
13-
if (proto === null) return true;
14-
if (!Object.prototype.hasOwnProperty.call(proto, 'constructor')) return false;
15-
const ctor = proto.constructor;
16-
if (typeof ctor !== 'function') return ctor;
17-
// `ctor === Object` but this works across contexts
18-
// (Object is special because Object.__proto__.__proto__ === Object.prototype),
19-
const ctorPrototype = Object.getPrototypeOf(ctor);
20-
return Object.getPrototypeOf(ctorPrototype) === ctor.prototype;
21-
}
22-
23-
export function redact<T>(message: T): T {
6+
export function redact<T>(
7+
message: T,
8+
secrets: Secret[] | undefined = undefined,
9+
): T {
2410
if (isPlainObject(message)) {
2511
// recursively walk through all values of an object
26-
return Object.fromEntries(
27-
Object.entries(message).map(([key, value]) => [key, redact(value)]),
12+
const newMessage = Object.fromEntries(
13+
Object.entries(message).map(([key, value]) => [
14+
key,
15+
redact(value, secrets),
16+
]),
2817
) as T;
18+
19+
// make sure we inherit the prototype so we don't add new behaviour to the object
20+
// nobody is expecting
21+
return Object.setPrototypeOf(
22+
newMessage,
23+
Object.getPrototypeOf(message) as object | null,
24+
);
2925
}
26+
3027
if (Array.isArray(message)) {
3128
// walk through array and redact each value
32-
return message.map(redact) as T;
29+
return message.map((msg)=>redact(msg,secrets)) as T;
3330
}
3431
if (typeof message !== 'string') {
3532
// all non-string types can be safely returned
3633
return message;
3734
}
35+
36+
if (secrets) {
37+
message = redactSecretsOnString(message, secrets);
38+
}
39+
3840
// apply all available regexes to the string
3941
for (const [regex, replacement] of regexes) {
4042
// The type here isn't completely accurate in case `T` is a specific string template
@@ -45,3 +47,4 @@ export function redact<T>(message: T): T {
4547
}
4648

4749
export default redact;
50+
export type { Secret } from './secrets';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { expect } from 'chai';
2+
import { SECRET_KIND } from './secrets';
3+
import type { SecretKind } from './secrets';
4+
import { redact } from './index';
5+
6+
describe('dictionary-based secret redaction', function () {
7+
for (const kind of SECRET_KIND) {
8+
it(`redacts content of kind '${kind}'`, function () {
9+
const secret = '123456';
10+
const content = '123456';
11+
12+
const redacted = redact(content, [{ value: secret, kind: kind }]);
13+
14+
expect(redacted).equal(`<${kind}>`);
15+
});
16+
}
17+
18+
for (const invalidValue of [null, undefined, false, 0]) {
19+
it(`returns itself on an invalid value like ${String(invalidValue)}`, function () {
20+
expect(redact(invalidValue as any, [])).equal(invalidValue);
21+
});
22+
}
23+
24+
it('redacts multiple coincidences of a secret', function () {
25+
const secret = '123456';
26+
const content = '123456 abc 123456';
27+
28+
const redacted = redact(content, [{ value: secret, kind: 'password' }]);
29+
30+
expect(redacted).to.equal('<password> abc <password>');
31+
});
32+
33+
it('rejects unknown types', function () {
34+
expect(() =>
35+
redact('some content to redact', [
36+
{
37+
value: 'some',
38+
kind: 'invalid' as SecretKind,
39+
},
40+
]),
41+
).throw();
42+
});
43+
44+
it('redacts secrets in between word boundaries', function () {
45+
const secret = '123456';
46+
const content = '.123456.';
47+
48+
const redacted = redact(content, [{ value: secret, kind: 'password' }]);
49+
50+
expect(redacted).equal('.<password>.');
51+
});
52+
53+
it('does not redact content that seems a secret inside another word', function () {
54+
const secret = '123456';
55+
const content = 'abc123456def';
56+
57+
const redacted = redact(content, [{ value: secret, kind: 'password' }]);
58+
59+
expect(redacted).equal('abc123456def');
60+
});
61+
62+
it('escapes values so using it in regexes is safe', function () {
63+
const secret = '.+';
64+
const content = '.abcdef.';
65+
66+
const redacted = redact(content, [{ value: secret, kind: 'password' }]);
67+
68+
expect(redacted).equal('.abcdef.');
69+
});
70+
71+
it('redacts on arrays', function () {
72+
const secret = 'abc';
73+
const content = ['abc', 'cbd'];
74+
75+
const redacted = redact(content, [{ value: secret, kind: 'password' }]);
76+
77+
expect(redacted).deep.equal(['<password>', 'cbd']);
78+
});
79+
80+
it('redacts on objects', function () {
81+
const pwdSecret = '123456';
82+
const usrSecret = 'admin';
83+
84+
const content = { pwd: pwdSecret, usr: usrSecret };
85+
86+
const redacted = redact(content, [
87+
{ value: pwdSecret, kind: 'password' },
88+
{ value: usrSecret, kind: 'user' },
89+
]);
90+
91+
expect(redacted).deep.equal({
92+
pwd: '<password>',
93+
usr: '<user>',
94+
});
95+
});
96+
});

‎packages/mongodb-redact/src/secrets.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// RegExp.escape is not widely available yet, so we are going to use regexp.escape
2+
// as it's extremely simple and at some point we can get rid of it easily.
3+
import escape from 'regexp.escape';
4+
5+
export const SECRET_KIND = [
6+
'base64',
7+
'private key',
8+
'user',
9+
'password',
10+
'email',
11+
'ip',
12+
'url',
13+
'mongodb uri',
14+
] as const;
15+
16+
export type SecretKind = (typeof SECRET_KIND)[number];
17+
18+
export type Secret = {
19+
readonly value: string;
20+
readonly kind: SecretKind;
21+
};
22+
23+
export function redactSecretsOnString<T extends string>(
24+
content: T,
25+
secrets: Secret[],
26+
): T {
27+
let result = content;
28+
for (const { value, kind } of secrets) {
29+
if (!SECRET_KIND.includes(kind)) {
30+
throw new Error(
31+
`Unknown secret kind ${kind}. Valid types: ${SECRET_KIND.join(', ')}`,
32+
);
33+
}
34+
35+
const regex = new RegExp(`\\b${escape(value)}\\b`, 'g');
36+
result = result.replace(regex, `<${kind}>`) as T;
37+
}
38+
39+
return result;
40+
}

‎packages/mongodb-redact/src/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const plainObjectTag = Object.prototype.toString.call({});
2+
3+
export function isPlainObject(val: unknown): val is object {
4+
if (
5+
typeof val !== 'object' ||
6+
!val ||
7+
Object.prototype.toString.call(val) !== plainObjectTag
8+
) {
9+
return false;
10+
}
11+
const proto = Object.getPrototypeOf(val);
12+
if (proto === null) return true;
13+
if (!Object.prototype.hasOwnProperty.call(proto, 'constructor')) return false;
14+
const ctor = proto.constructor;
15+
if (typeof ctor !== 'function') return false;
16+
// `ctor === Object` but this works across contexts
17+
// (Object is special because Object.__proto__.__proto__ === Object.prototype),
18+
const ctorPrototype = Object.getPrototypeOf(ctor);
19+
return Object.getPrototypeOf(ctorPrototype) === ctor.prototype;
20+
}

‎packages/mongodb-redact/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"compilerOptions": {
44
"outDir": "dist",
55
"allowJs": true,
6-
"strict": true
6+
"strict": true,
7+
"esModuleInterop": true
78
},
89
"include": ["src/**/*"],
910
"exclude": ["./src/**/*.spec.*"]

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /