Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(NODE-3627): Enable flexible BSON validation for server error key containing invalid utf-8 #3054

Merged
merged 10 commits into from
Nov 30, 2021
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"email": "[email protected]"
},
"dependencies": {
"bson": "^4.5.4",
"bson": "^4.6.0",
"denque": "^2.0.1",
"mongodb-connection-string-url": "^2.2.0"
},
Expand Down
1 change: 1 addition & 0 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface BSONSerializeOptions
> {
/** Return BSON filled buffers from operations */
raw?: boolean;
validation?: { utf8: boolean | Record<string, true> | Record<string, false> };
}

export function pluckBSONSerializeOptions(options: BSONSerializeOptions): BSONSerializeOptions {
Expand Down
10 changes: 9 additions & 1 deletion src/cmap/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ export interface MessageHeader {
export interface OpResponseOptions extends BSONSerializeOptions {
raw?: boolean;
documentsReturnedIn?: string | null;
validation?: { utf8: boolean | Record<string, true> | Record<string, false> };
}

/** @internal */
Expand Down Expand Up @@ -837,13 +838,20 @@ export class BinMsg {
const promoteValues = options.promoteValues ?? this.opts.promoteValues;
const promoteBuffers = options.promoteBuffers ?? this.opts.promoteBuffers;
const bsonRegExp = options.bsonRegExp ?? this.opts.bsonRegExp;
dariakp marked this conversation as resolved.
Show resolved Hide resolved
if (options.validation && Object.keys(options.validation.utf8)[0] !== 'writeErrors') {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
throw new MongoInvalidArgumentError(
'Can only toggle validation settings for writeErrors key'
);
}
const validation = options.validation ?? { utf8: { writeErrors: false } };

// Set up the options
const _options: BSONSerializeOptions = {
promoteLongs,
promoteValues,
promoteBuffers,
bsonRegExp
bsonRegExp,
validation
};

while (this.index < this.data.length) {
Expand Down
4 changes: 4 additions & 0 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,10 @@ export const OPTIONS = {
default: false,
type: 'boolean'
},
validation: {
default: { utf8: { writeErrors: false } },
type: 'record'
},
readConcern: {
transform({ values: [value], options }) {
if (value instanceof ReadConcern || isRecord(value, ['level'] as const)) {
Expand Down
3 changes: 2 additions & 1 deletion test/types/bson.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ type PermittedBSONOptionKeys =
| 'promoteValues'
| 'bsonRegExp'
| 'fieldsAsRaw'
| 'raw';
| 'raw'
| 'validation';

const keys = null as unknown as PermittedBSONOptionKeys;
// creates an explicit allow list assertion
Expand Down
151 changes: 151 additions & 0 deletions test/unit/commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { expect } from 'chai';
import { BinMsg, MessageHeader, OpResponseOptions } from '../../src/cmap/commands';
import { BSONError } from 'bson';
import * as BSON from '../../src/bson';

const msgHeader: MessageHeader = {
length: 735,
requestId: 14704565,
responseTo: 4,
opCode: 2013
};

// when top-level key writeErrors contains an error message that has invalid utf8
const invalidUtf8ErrorMsg =
'0000000000ca020000106e00000000000477726974654572726f727300a50200000330009d02000010696e646578000000000010636f646500f82a0000036b65795061747465726e000f0000001074657874000100000000036b657956616c756500610100000274657874005201000064e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e298830000026572726d736700f1000000453131303030206475706c6963617465206b6579206572726f7220636f6c6c656374696f6e3a20626967646174612e7465737420696e6465783a20746578745f3120647570206b65793a207b20746578743a202264e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e2982e2e2e22207d000000016f6b00000000000000f03f00';
const msgBodyInvalidUtf8WriteErrors = Buffer.from(invalidUtf8ErrorMsg, 'hex');
const invalidUtf8ErrorMsgDeserializeInput = Buffer.from(invalidUtf8ErrorMsg.substring(10), 'hex');
const invalidUtf8InWriteErrorsJSON = {
n: 0,
writeErrors: [
{
index: 0,
code: 11000,
keyPattern: {
text: 1
},
keyValue: {
text: 'd☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃'
},
errmsg:
'E11000 duplicate key error collection: bigdata.test index: text_1 dup key: { text: "d☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃�..." }'
}
],
ok: 1
};

// when another top-level key besides writeErrors has invalid utf8
const nKeyWithInvalidUtf8 =
'0000000000cc020000026e0005000000f09f98ff000477726974654572726f727300a60200000330009e02000010696e646578000000000010636f646500f82a0000036b65795061747465726e000f0000001074657874000100000000036b657956616c756500610100000274657874005201000064e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e298830000026572726d736700f2000000453131303030206475706c6963617465206b6579206572726f7220636f6c6c656374696f6e3a20626967646174612e7465737420696e6465783a20746578745f3120647570206b65793a207b20746578743a202264e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883e29883efbfbd2e2e2e22207d000000106f6b000100000000';
const nKeyWithInvalidUtf8DeserializeInput = Buffer.from(nKeyWithInvalidUtf8.substring(10), 'hex');
const msgBodyNKeyWithInvalidUtf8 = Buffer.from(nKeyWithInvalidUtf8, 'hex');
const invalidUtf8InNKeyJSON = {
n: '��',
writeErrors: [
{
index: 0,
code: 11000,
keyPattern: {
text: 1
},
keyValue: {
text: 'd☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃'
},
errmsg:
'E11000 duplicate key error collection: bigdata.test index: text_1 dup key: { text: "d☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃☃�..." }'
}
],
ok: 1
};

describe('BinMsg', function () {
context('when validation is disabled for writeErrors', function () {
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyInvalidUtf8WriteErrors
);
const options: OpResponseOptions = { validation: { utf8: { writeErrors: false } } };

it('contains replacement characters for invalid utf8 in writeError object', function () {
expect(BSON.deserialize(invalidUtf8ErrorMsgDeserializeInput, options)).to.deep.equals(
invalidUtf8InWriteErrorsJSON
);
});

it('should not throw invalid utf8 error', function () {
expect(() => binMsgInvalidUtf8ErrorMsg.parse(options)).to.not.throw();
});
});

it('should throw error if trying to toggle validation settings for keys other than writeErrors', function () {
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyInvalidUtf8WriteErrors
);
expect(() => binMsgInvalidUtf8ErrorMsg.parse({ validation: { utf8: { n: false } } })).to.throw(
Error,
'Can only toggle validation settings for writeErrors key'
);
});

it('should by default disable validation for writeErrors if no validation specified', function () {
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyInvalidUtf8WriteErrors
);
const options = {
bsonRegExp: false,
promoteBuffers: false,
promoteLongs: true,
promoteValues: true
};
expect(() => binMsgInvalidUtf8ErrorMsg.parse(options)).to.not.throw();
});

context(
'when another key has invalid utf8 and validation is enabled for writeErrors',
function () {
const binMsgAnotherKeyWithInvalidUtf8 = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyNKeyWithInvalidUtf8
);
const option: OpResponseOptions = { validation: { utf8: { writeErrors: true } } };

it('should not throw invalid utf8 error', function () {
expect(() => binMsgAnotherKeyWithInvalidUtf8.parse(option)).to.not.throw();
});

it('contains replacement characters for invalid utf8 key', function () {
expect(BSON.deserialize(nKeyWithInvalidUtf8DeserializeInput, option)).to.deep.equals(
invalidUtf8InNKeyJSON
);
});
}
);

it('should throw invalid utf8 error when validation enabled for writeErrors', function () {
const binMsgInvalidUtf8ErrorMsg = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyInvalidUtf8WriteErrors
);
expect(() =>
binMsgInvalidUtf8ErrorMsg.parse({ validation: { utf8: { writeErrors: true } } })
).to.throw(BSONError, 'Invalid UTF-8 string in BSON document');
});

it('should throw error when another key has invalid utf8 and writeErrors is not validated', function () {
const binMsgAnotherKeyWithInvalidUtf8 = new BinMsg(
Buffer.alloc(0),
msgHeader,
msgBodyNKeyWithInvalidUtf8
);
expect(() =>
binMsgAnotherKeyWithInvalidUtf8.parse({ validation: { utf8: { writeErrors: false } } })
).to.throw(BSONError, 'Invalid UTF-8 string in BSON document');
});
});