Skip to content

Commit 7a507f0

Browse files
authored
fix(NODE-3627): Enable flexible BSON validation for server error key containing invalid utf-8 (#3054)
1 parent 307d623 commit 7a507f0

File tree

5 files changed

+156
-16
lines changed

5 files changed

+156
-16
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"email": "[email protected]"
3333
},
3434
"dependencies": {
35-
"bson": "^4.5.4",
35+
"bson": "^4.6.0",
3636
"denque": "^2.0.1",
3737
"mongodb-connection-string-url": "^2.2.0"
3838
},

src/bson.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface BSONSerializeOptions
5151
| 'cacheFunctionsCrc32'
5252
| 'allowObjectSmallerThanBufferSize'
5353
| 'index'
54+
| 'validation'
5455
> {
5556
/** Return BSON filled buffers from operations */
5657
raw?: boolean;

src/cmap/commands.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ export interface MessageHeader {
469469
export interface OpResponseOptions extends BSONSerializeOptions {
470470
raw?: boolean;
471471
documentsReturnedIn?: string | null;
472+
// For now we use this internally to only prevent writeErrors from crashing the driver
473+
validation?: { utf8: { writeErrors: boolean } };
472474
}
473475

474476
/** @internal */
@@ -837,22 +839,24 @@ export class BinMsg {
837839
const promoteValues = options.promoteValues ?? this.opts.promoteValues;
838840
const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
839841
const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
842+
const validation = options.validation ?? { utf8: { writeErrors: false } };
840843

841844
// Set up the options
842-
const _options: BSONSerializeOptions = {
845+
const bsonOptions: BSONSerializeOptions = {
843846
promoteLongs,
844847
promoteValues,
845848
promoteBuffers,
846-
bsonRegExp
847-
};
849+
bsonRegExp,
850+
validation
851+
// Due to the strictness of the BSON libraries validation option we need this cast
852+
} as BSONSerializeOptions & { validation: { utf8: { writeErrors: boolean } } };
848853

849854
while (this.index < this.data.length) {
850855
const payloadType = this.data.readUInt8(this.index++);
851856
if (payloadType === 0) {
852857
const bsonSize = this.data.readUInt32LE(this.index);
853858
const bin = this.data.slice(this.index, this.index + bsonSize);
854-
this.documents.push(raw ? bin : BSON.deserialize(bin, _options));
855-
859+
this.documents.push(raw ? bin : BSON.deserialize(bin, bsonOptions));
856860
this.index += bsonSize;
857861
} else if (payloadType === 1) {
858862
// It was decided that no driver makes use of payload type 1
@@ -865,9 +869,8 @@ export class BinMsg {
865869
if (this.documents.length === 1 && documentsReturnedIn != null && raw) {
866870
const fieldsAsRaw: Document = {};
867871
fieldsAsRaw[documentsReturnedIn] = true;
868-
_options.fieldsAsRaw = fieldsAsRaw;
869-
870-
const doc = BSON.deserialize(this.documents[0] as Buffer, _options);
872+
bsonOptions.fieldsAsRaw = fieldsAsRaw;
873+
const doc = BSON.deserialize(this.documents[0] as Buffer, bsonOptions);
871874
this.documents = [doc];
872875
}
873876

test/unit/commands.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { expect } from 'chai';
2+
import { BinMsg, MessageHeader } from '../../src/cmap/commands';
3+
import { BSONError } from 'bson';
4+
import * as BSON from '../../src/bson';
5+
6+
const msgHeader: MessageHeader = {
7+
length: 735,
8+
requestId: 14704565,
9+
responseTo: 4,
10+
opCode: 2013
11+
};
12+
13+
// when top-level key writeErrors contains an error message that has invalid utf8
14+
const invalidUtf8ErrorMsg =
15+
'0000000000ca020000106e00000000000477726974654572726f727300a50200000330009d02000010696e646578000000000010636f646500f82a0000036b65795061747465726e000f0000001074657874000100000000036b657956616c756500610100000274657874005201000064e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e298830000026572726d736700f1000000453131303030206475706c6963617465206b6579206572726f7220636f6c6c656374696f6e3a20626967646174612e7465737420696e6465783a20746578745f3120647570206b65793a207b20746578743a202264e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e2982e2e2e22207d000000016f6b00000000000000f03f00';
16+
const msgBodyInvalidUtf8WriteErrors = Buffer.from(invalidUtf8ErrorMsg, 'hex');
17+
const invalidUtf8ErrorMsgDeserializeInput = Buffer.from(invalidUtf8ErrorMsg.substring(10), 'hex');
18+
const invalidUtf8InWriteErrorsJSON = {
19+
n: 0,
20+
writeErrors: [
21+
{
22+
index: 0,
23+
code: 11000,
24+
keyPattern: {
25+
text: 1
26+
},
27+
keyValue: {
28+
text: 'd☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃'
29+
},
30+
errmsg:
31+
'E11000 duplicate key error collection: bigdata.test index: text_1 dup key: { text: "d☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃�..." }'
32+
}
33+
],
34+
ok: 1
35+
};
36+
37+
// when another top-level key besides writeErrors has invalid utf8
38+
const nKeyWithInvalidUtf8 =
39+
'0000000000cc020000026e0005000000f09f98ff000477726974654572726f727300a60200000330009e02000010696e646578000000000010636f646500f82a0000036b65795061747465726e000f0000001074657874000100000000036b657956616c756500610100000274657874005201000064e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e298830000026572726d736700f2000000453131303030206475706c6963617465206b6579206572726f7220636f6c6c656374696f6e3a20626967646174612e7465737420696e6465783a20746578745f3120647570206b65793a207b20746578743a202264e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883efbfbd2e2e2e22207d000000106f6b000100000000';
40+
const nKeyWithInvalidUtf8DeserializeInput = Buffer.from(nKeyWithInvalidUtf8.substring(10), 'hex');
41+
const msgBodyNKeyWithInvalidUtf8 = Buffer.from(nKeyWithInvalidUtf8, 'hex');
42+
const invalidUtf8InNKeyJSON = {
43+
n: '��',
44+
writeErrors: [
45+
{
46+
index: 0,
47+
code: 11000,
48+
keyPattern: {
49+
text: 1
50+
},
51+
keyValue: {
52+
text: 'd☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃'
53+
},
54+
errmsg:
55+
'E11000 duplicate key error collection: bigdata.test index: text_1 dup key: { text: "d☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃�..." }'
56+
}
57+
],
58+
ok: 1
59+
};
60+
61+
describe('BinMsg BSON utf8 validation', () => {
62+
context('when validation is disabled for writeErrors', () => {
63+
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
64+
Buffer.alloc(0),
65+
msgHeader,
66+
msgBodyInvalidUtf8WriteErrors
67+
);
68+
const options = { validation: { utf8: { writeErrors: false } as const } };
69+
70+
it('contains replacement characters for invalid utf8 in writeError object', () => {
71+
expect(BSON.deserialize(invalidUtf8ErrorMsgDeserializeInput, options)).to.deep.equals(
72+
invalidUtf8InWriteErrorsJSON
73+
);
74+
});
75+
76+
it('should not throw invalid utf8 error', () => {
77+
expect(() => binMsgInvalidUtf8ErrorMsg.parse(options)).to.not.throw();
78+
});
79+
});
80+
81+
it('should by default disable validation for writeErrors if no validation specified', () => {
82+
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
83+
Buffer.alloc(0),
84+
msgHeader,
85+
msgBodyInvalidUtf8WriteErrors
86+
);
87+
const options = {
88+
bsonRegExp: false,
89+
promoteBuffers: false,
90+
promoteLongs: true,
91+
promoteValues: true
92+
};
93+
expect(() => binMsgInvalidUtf8ErrorMsg.parse(options)).to.not.throw();
94+
});
95+
96+
context('when another key has invalid utf8 and validation is enabled for writeErrors', () => {
97+
const binMsgAnotherKeyWithInvalidUtf8 = new BinMsg(
98+
Buffer.alloc(0),
99+
msgHeader,
100+
msgBodyNKeyWithInvalidUtf8
101+
);
102+
const options = { validation: { utf8: { writeErrors: true } as const } };
103+
104+
it('should not throw invalid utf8 error', () => {
105+
expect(() => binMsgAnotherKeyWithInvalidUtf8.parse(options)).to.not.throw();
106+
});
107+
108+
it('contains replacement characters for invalid utf8 key', () => {
109+
expect(BSON.deserialize(nKeyWithInvalidUtf8DeserializeInput, options)).to.deep.equals(
110+
invalidUtf8InNKeyJSON
111+
);
112+
});
113+
});
114+
115+
it('should throw invalid utf8 error when validation enabled for writeErrors', () => {
116+
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
117+
Buffer.alloc(0),
118+
msgHeader,
119+
msgBodyInvalidUtf8WriteErrors
120+
);
121+
expect(() =>
122+
binMsgInvalidUtf8ErrorMsg.parse({ validation: { utf8: { writeErrors: true } } })
123+
).to.throw(BSONError, 'Invalid UTF-8 string in BSON document');
124+
});
125+
126+
it('should throw error when another key has invalid utf8 and writeErrors is not validated', () => {
127+
const binMsgAnotherKeyWithInvalidUtf8 = new BinMsg(
128+
Buffer.alloc(0),
129+
msgHeader,
130+
msgBodyNKeyWithInvalidUtf8
131+
);
132+
expect(() =>
133+
binMsgAnotherKeyWithInvalidUtf8.parse({ validation: { utf8: { writeErrors: false } } })
134+
).to.throw(BSONError, 'Invalid UTF-8 string in BSON document');
135+
});
136+
});

0 commit comments

Comments
 (0)