From 89a0b429db016f0cc53220d6d55c494c6bab0260 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Thu, 11 Nov 2021 19:40:47 -0500 Subject: [PATCH 01/13] feat(NODE-3740):Implement root and top level key utf-8 validation settings for BSON --- src/parser/deserializer.ts | 115 ++++++++++++++++--- test/node/tools/utils.js | 25 ++++ test/node/utf8_tests.js | 228 +++++++++++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 test/node/utf8_tests.js diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 3ba05499..30479971 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -45,6 +45,8 @@ export interface DeserializeOptions { index?: number; raw?: boolean; + /** allows for opt-in utf-8 validation */ + validation?: Document; } // Internal long versions @@ -102,7 +104,8 @@ function deserializeObject( buffer: Buffer, index: number, options: DeserializeOptions, - isArray = false + isArray = false, + nestedKey?: boolean ) { const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions']; const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions']; @@ -120,6 +123,45 @@ function deserializeObject( const promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs']; const promoteValues = options['promoteValues'] == null ? true : options['promoteValues']; + // Ensures default validation option if none given + const validation = options['validation'] == null ? { utf8: true } : options['validation']; + + // Shows if global utf8 validation is enabled or disabled + let globalUTFValidation = true; + // Reflects utf8 validation boolean regardless of global or specific key validation + let uniformBool: boolean; + // Set of keys either to enable or disable validation on + const utf8KeysSet = new Set(); + + // Check for boolean uniformity and empty validation option + const keys = validation.utf8; + if (typeof keys !== 'boolean') { + globalUTFValidation = false; + const vals = Object.values(keys); + if (vals.length !== 0) { + // Ensures boolean uniformity in utf-8 validation (all true or all false) + if (typeof vals[0] === 'boolean') { + uniformBool = vals[0]; + if (!vals.every(item => item === uniformBool)) { + throw new BSONError( + 'Invalid UTF-8 validation option - keys must be all true or all false' + ); + } + } else { + throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); + } + } else { + throw new BSONError('validation option is empty'); + } + } else { + uniformBool = keys; + } + + // Add keys to set that will either be validated or not based on uniformBool + if ((!uniformBool && !globalUTFValidation) || (uniformBool && !globalUTFValidation)) { + Object.keys(keys).forEach(key => utf8KeysSet.add(key)); + } + // Set the start index const startIndex = index; @@ -158,7 +200,19 @@ function deserializeObject( // If are at the end of the buffer there is a problem with the document if (i >= buffer.byteLength) throw new BSONError('Bad BSON Document: illegal CString'); + + // Represents the key const name = isArray ? arrayIndex++ : buffer.toString('utf8', index, i); + + // keyValidate is true if the key should be validated, false otherwise + let keyValidate = true; + if (utf8KeysSet.has(name) && !uniformBool) { + keyValidate = false; + } + if (nestedKey != null) { + keyValidate = nestedKey; + } + if (isPossibleDBRef !== false && (name as string)[0] === '$') { isPossibleDBRef = allowedDBRefKeys.test(name as string); } @@ -179,9 +233,7 @@ function deserializeObject( ) { throw new BSONError('bad string length in bson'); } - - value = getValidatedString(buffer, index, index + stringSize - 1); - + value = getValidatedString(buffer, index, index + stringSize - 1, validation, keyValidate); index = index + stringSize; } else if (elementType === constants.BSON_DATA_OID) { const oid = Buffer.alloc(12); @@ -234,7 +286,7 @@ function deserializeObject( if (raw) { value = buffer.slice(index, index + objectSize); } else { - value = deserializeObject(buffer, _index, options, false); + value = deserializeObject(buffer, _index, options, false, keyValidate); } index = index + objectSize; @@ -463,7 +515,13 @@ function deserializeObject( ) { throw new BSONError('bad string length in bson'); } - const symbol = getValidatedString(buffer, index, index + stringSize - 1); + const symbol = getValidatedString( + buffer, + index, + index + stringSize - 1, + validation, + keyValidate + ); value = promoteValues ? symbol : new BSONSymbol(symbol); index = index + stringSize; } else if (elementType === constants.BSON_DATA_TIMESTAMP) { @@ -496,7 +554,13 @@ function deserializeObject( ) { throw new BSONError('bad string length in bson'); } - const functionString = getValidatedString(buffer, index, index + stringSize - 1); + const functionString = getValidatedString( + buffer, + index, + index + stringSize - 1, + validation, + keyValidate + ); // If we are evaluating the functions if (evalFunctions) { @@ -541,7 +605,13 @@ function deserializeObject( } // Javascript function - const functionString = getValidatedString(buffer, index, index + stringSize - 1); + const functionString = getValidatedString( + buffer, + index, + index + stringSize - 1, + validation, + keyValidate + ); // Update parse index position index = index + stringSize; // Parse the element @@ -596,8 +666,10 @@ function deserializeObject( ) throw new BSONError('bad string length in bson'); // Namespace - if (!validateUtf8(buffer, index, index + stringSize - 1)) { - throw new BSONError('Invalid UTF-8 string in BSON document'); + if (validation != null && validation.utf8) { + if (!validateUtf8(buffer, index, index + stringSize - 1)) { + throw new BSONError('Invalid UTF-8 string in BSON document'); + } } const namespace = buffer.toString('utf8', index, index + stringSize - 1); // Update parse index position @@ -670,14 +742,25 @@ function isolateEval( return functionCache[functionString].bind(object); } -function getValidatedString(buffer: Buffer, start: number, end: number) { +function getValidatedString( + buffer: Buffer, + start: number, + end: number, + validation: Document, + check: boolean +) { const value = buffer.toString('utf8', start, end); - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 0xfffd) { - if (!validateUtf8(buffer, start, end)) { - throw new BSONError('Invalid UTF-8 string in BSON document'); + // if utf8 validation is on, do the check + if (check) { + if (validation.utf8 != null && validation.utf8) { + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) === 0xfffd) { + if (!validateUtf8(buffer, start, end)) { + throw new BSONError('Invalid UTF-8 string in BSON document'); + } + break; + } } - break; } } return value; diff --git a/test/node/tools/utils.js b/test/node/tools/utils.js index 8cea5bf5..5a0bb704 100644 --- a/test/node/tools/utils.js +++ b/test/node/tools/utils.js @@ -125,3 +125,28 @@ const bufferFromHexArray = array => { }; exports.bufferFromHexArray = bufferFromHexArray; + +/** + * A helper to calculate the byte size of a string (including null) + * + * ```js + * const x = stringToUTF8HexBytes('ab') // { x: '03000000616200' } + * + * @param string - representing what you want to encode into BSON + * @returns BSON string with byte size encoded + */ +const stringToUTF8HexBytes = str => { + var b = Buffer.from(str, 'utf8'); + var len = b.byteLength; + var out = Buffer.alloc(len + 4 + 1); + out.writeInt32LE(len + 1, 0); + out.set(b, 4); + return out.toString('hex'); +}; + +exports.stringToUTF8HexBytes = stringToUTF8HexBytes; + +exports.isBrowser = function () { + // eslint-disable-next-line no-undef + return typeof window === 'object' && typeof window['navigator'] === 'object'; +}; diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js new file mode 100644 index 00000000..8308f065 --- /dev/null +++ b/test/node/utf8_tests.js @@ -0,0 +1,228 @@ +'use strict'; + +const { Buffer } = require('buffer'); +const BSON = require('../register-bson'); +const { isBrowser, bufferFromHexArray, stringToUTF8HexBytes } = require('./tools/utils'); +const BSONError = BSON.BSONError; + +describe.only('UTF8 validation', function () { + it('should throw error if true and false mixed for validation option passed in', function () { + let mixedTrueFalse1 = { validation: { utf8: { a: false, b: true } } }; + let mixedTrueFalse2 = { validation: { utf8: { a: true, b: true, c: false } } }; + let allTrue = { validation: { utf8: { a: true, b: true, c: true } } }; + let allFalse = { validation: { utf8: { a: false, b: false, c: false, d: false } } }; + let sampleValidUTF8 = BSON.serialize('abcdedede'); + expect(() => BSON.deserialize(sampleValidUTF8, mixedTrueFalse1)).to.throw( + BSONError, + 'Invalid UTF-8 validation option - keys must be all true or all false' + ); + expect(() => BSON.deserialize(sampleValidUTF8, mixedTrueFalse2)).to.throw( + BSONError, + 'Invalid UTF-8 validation option - keys must be all true or all false' + ); + expect(() => BSON.deserialize(sampleValidUTF8, allTrue)).to.not.throw(); + expect(() => BSON.deserialize(sampleValidUTF8, allFalse)).to.not.throw(); + }); + + it('should throw error if empty utf8 validation option passed in', function () { + var doc = { a: 'validation utf8 option cant be empty' }; + let emptyUTF8validation = { validation: { utf8: {} } }; + const serialized = BSON.serialize(doc); + expect(() => BSON.deserialize(serialized, emptyUTF8validation)).to.throw( + BSONError, + 'validation option is empty' + ); + }); + + // Invalid utf8 examples + const invalidUtf8 = bufferFromHexArray([ + '02', // utf8 type + '696e76616c69647574663800', // key 'invalidutf8' + '09000000', // size of bytes + null + '6869f09f90627965', // value 'hi' + broken byte sequence + 'bye' + '00' + ]); + const invalidUTF8str1 = Buffer.from('0E00000002610002000000E90000', 'hex'); + const invalidUTF8str2 = Buffer.from( + '1A0000000C610002000000E90056E1FC72E0C917E9C471416100', + 'hex' + ); + const invalidUtf8SingleKey = [invalidUtf8, invalidUTF8str1, invalidUTF8str2]; + + it('should enforce UTF8 validation by default if no validation option specified', function () { + for (const example of invalidUtf8SingleKey) { + expect(() => BSON.deserialize(example)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } + }); + + it('should disable UTF8 validation on any single key if validation option sets utf8: false', function () { + let validationOption = { validation: { utf8: false } }; + for (const example of invalidUtf8SingleKey) { + expect(() => BSON.deserialize(example, validationOption)).to.not.throw(); + } + }); + + it('should enable UTF8 validation on any key if validation option sets utf8: true', function () { + let validationOption = { validation: { utf8: true } }; + for (const example of invalidUtf8SingleKey) { + expect(() => BSON.deserialize(example, validationOption)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } + }); + + const invalidUtf8ManyKeys = bufferFromHexArray([ + '02', // utf8 type + Buffer.from('validUtf8Chars', 'utf8').toString('hex') + '00', + stringToUTF8HexBytes('abc'), + '02', + Buffer.from('invalidUtf8', 'utf8').toString('hex') + '00', + '090000006869f09f9062796500', // value 'hi' + broken byte sequence + 'bye' + '02', + Buffer.from('invalidUtf82', 'utf8').toString('hex') + '00', + '0a000000f09f90f09f9062796500' // 2 broken byte sequences + 'bye' + ]); + + const expectedObjWithReplacements = { + validUtf8Chars: 'abc', + invalidUtf8: 'hi�bye', + invalidUtf82: '��bye' + }; + + const testOutputObjects = [ + { + behavior: 'enable global UTF8 validation', + validation: { validation: { utf8: true } }, + errorExpect: true + }, + { + behavior: 'globally disable UTF8 validation', + validation: { validation: { utf8: false } }, + errorExpect: false + }, + { + behavior: 'enable UTF8 validation for specified key and disable for other keys', + validation: { validation: { utf8: { invalidUtf8: true } } }, + errorExpect: true + }, + { + behavior: 'disable UTF8 validation for specified key and enable for other keys', + validation: { validation: { utf8: { invalidUtf82: false } } }, + errorExpect: true + }, + { + behavior: 'disable UTF8 validation for all specified keys', + validation: { validation: { utf8: { invalidUtf8: false, invalidUtf82: false } } }, + errorExpect: false + } + ]; + + for (const { behavior, validation, errorExpect } of testOutputObjects) { + it(`should ${behavior} for object with invalid utf8 in top level keys`, function () { + if (isBrowser()) this.skip(); + const encodedObj = invalidUtf8ManyKeys; + if (errorExpect) { + expect(() => BSON.deserialize(encodedObj, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } else { + expect(BSON.deserialize(encodedObj, validation)).to.deep.equals( + expectedObjWithReplacements + ); + } + }); + } + + const invalidUtf8NestedKeys = bufferFromHexArray([ + '03' + Buffer.from('a', 'utf8').toString('hex') + '00', // key a + '3a000000', + '03' + Buffer.from('a1', 'utf8').toString('hex') + '00', // subkey a1 + '31000000', + '02' + Buffer.from('a11', 'utf8').toString('hex') + '00', // nested subkeys + stringToUTF8HexBytes('abcdefg'), + '02' + Buffer.from('invalidUtf81', 'utf8').toString('hex') + '00', + '090000006869f09f9062796500', + '00', + '00', + '03' + Buffer.from('b', 'utf8').toString('hex') + '00', // key b + '30000000', + '02' + Buffer.from('b1', 'utf8').toString('hex') + '00', // subkey b1 + stringToUTF8HexBytes('abcdefg'), + '02' + Buffer.from('invalidUtf82', 'utf8').toString('hex') + '00', // subkey invalidUtf82 + '090000006869f09f9062796500' + '00', + '02' + Buffer.from('invalidUtf83', 'utf8').toString('hex') + '00', // key invalidUtf83 + '090000006869f09f9062796500' + ]); + + const expectedNestedKeysObj = { + a: { + a1: { + a11: 'abcdefg', + invalidUtf81: 'hi�bye' + } + }, + b: { + b1: 'abcdefg', + invalidUtf82: 'hi�bye' + }, + invalidUtf83: 'hi�bye' + }; + + const testOutputObjectsNested = [ + { + behavior: 'enable global UTF8 validation', + validation: { validation: { utf8: true } }, + errorExpect: true + }, + { + behavior: 'globally disable UTF8 validation', + validation: { validation: { utf8: false } }, + errorExpect: false + }, + { + behavior: 'disable UTF8 validation for specified key and enable for other keys', + validation: { validation: { utf8: { a: false } } }, + errorExpect: true + }, + { + behavior: 'enable UTF8 validation for specified key and disable for other keys', + validation: { validation: { utf8: { a: true } } }, + errorExpect: true + }, + { + behavior: 'disable UTF8 validation on specified invalid keys', + validation: { validation: { utf8: { a: false, b: false } } }, + errorExpect: true + }, + { + behavior: 'disable UTF8 validation on all invalid keys', + validation: { + validation: { + utf8: { a: false, b: false, invalidUtf83: false } + } + }, + errorExpect: false + } + ]; + + for (const { behavior, validation, errorExpect } of testOutputObjectsNested) { + it(`should ${behavior} for object with invalid utf8 in nested keys`, function () { + if (isBrowser()) this.skip(); + if (errorExpect) { + expect(() => BSON.deserialize(invalidUtf8NestedKeys, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } else { + expect(BSON.deserialize(invalidUtf8NestedKeys, validation)).to.deep.equals( + expectedNestedKeysObj + ); + } + }); + } +}); From 860c2c8444f6ea0c24b306b80d22cc586a1932ef Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 12 Nov 2021 17:01:27 -0500 Subject: [PATCH 02/13] refactor: add appropriate test cases and more objects with invalid utf8s at different levels --- src/parser/deserializer.ts | 11 +- test/node/utf8_tests.js | 364 ++++++++++++++++++++++--------------- 2 files changed, 223 insertions(+), 152 deletions(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 30479971..c7610fd4 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -206,9 +206,16 @@ function deserializeObject( // keyValidate is true if the key should be validated, false otherwise let keyValidate = true; - if (utf8KeysSet.has(name) && !uniformBool) { - keyValidate = false; + if (globalUTFValidation) { + keyValidate = uniformBool; + } else { + if (utf8KeysSet.has(name)) { + keyValidate = uniformBool; + } else if (!utf8KeysSet.has(name)) { + keyValidate = !uniformBool; + } } + // if nested key, validate based on top level key if (nestedKey != null) { keyValidate = nestedKey; } diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index 8308f065..f5ec3d73 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -2,16 +2,23 @@ const { Buffer } = require('buffer'); const BSON = require('../register-bson'); -const { isBrowser, bufferFromHexArray, stringToUTF8HexBytes } = require('./tools/utils'); +const { isBrowser } = require('./tools/utils'); const BSONError = BSON.BSONError; describe.only('UTF8 validation', function () { - it('should throw error if true and false mixed for validation option passed in', function () { - let mixedTrueFalse1 = { validation: { utf8: { a: false, b: true } } }; - let mixedTrueFalse2 = { validation: { utf8: { a: true, b: true, c: false } } }; - let allTrue = { validation: { utf8: { a: true, b: true, c: true } } }; - let allFalse = { validation: { utf8: { a: false, b: false, c: false, d: false } } }; - let sampleValidUTF8 = BSON.serialize('abcdedede'); + // Test both browser shims and node which have different replacement mechanisms + const replacementChar = isBrowser() ? '���' : '�'; + const replacementString = `hi${replacementChar}bye`; + const twoCharReplacementStr = `${replacementChar}${replacementChar}bye`; + const sampleValidUTF8 = BSON.serialize({ + a: '😎', + b: 'valid utf8', + c: 12345 + }); + + it('should throw error if true and false mixed for validation option passed in with valid utf8 example', function () { + const mixedTrueFalse1 = { validation: { utf8: { a: false, b: true } } }; + const mixedTrueFalse2 = { validation: { utf8: { a: true, b: true, c: false } } }; expect(() => BSON.deserialize(sampleValidUTF8, mixedTrueFalse1)).to.throw( BSONError, 'Invalid UTF-8 validation option - keys must be all true or all false' @@ -20,207 +27,264 @@ describe.only('UTF8 validation', function () { BSONError, 'Invalid UTF-8 validation option - keys must be all true or all false' ); + }); + + it('should correctly handle validation if validation option contains all T or all F with valid utf8 example', function () { + let allTrue = { validation: { utf8: { a: true, b: true, c: true } } }; + let allFalse = { validation: { utf8: { a: false, b: false, c: false, d: false } } }; expect(() => BSON.deserialize(sampleValidUTF8, allTrue)).to.not.throw(); expect(() => BSON.deserialize(sampleValidUTF8, allFalse)).to.not.throw(); }); it('should throw error if empty utf8 validation option passed in', function () { var doc = { a: 'validation utf8 option cant be empty' }; - let emptyUTF8validation = { validation: { utf8: {} } }; const serialized = BSON.serialize(doc); + let emptyUTF8validation = { validation: { utf8: {} } }; expect(() => BSON.deserialize(serialized, emptyUTF8validation)).to.throw( BSONError, 'validation option is empty' ); }); - // Invalid utf8 examples - const invalidUtf8 = bufferFromHexArray([ - '02', // utf8 type - '696e76616c69647574663800', // key 'invalidutf8' - '09000000', // size of bytes + null - '6869f09f90627965', // value 'hi' + broken byte sequence + 'bye' - '00' - ]); - const invalidUTF8str1 = Buffer.from('0E00000002610002000000E90000', 'hex'); - const invalidUTF8str2 = Buffer.from( - '1A0000000C610002000000E90056E1FC72E0C917E9C471416100', - 'hex' - ); - const invalidUtf8SingleKey = [invalidUtf8, invalidUTF8str1, invalidUTF8str2]; - - it('should enforce UTF8 validation by default if no validation option specified', function () { - for (const example of invalidUtf8SingleKey) { - expect(() => BSON.deserialize(example)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' - ); + const testInputs = [ + { + description: 'object with valid utf8 top level keys', + buffer: Buffer.from( + '2e0000000276616c69644b65794368617200060000006162636465001076616c69644b65794e756d003930000000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + validKeyNum: 12345 + }, + containsInvalid: false + }, + { + description: 'object with invalid utf8 top level key', + buffer: Buffer.from( + '420000000276616c69644b657943686172000600000061626364650002696e76616c696455746638546f704c6576656c4b657900090000006869f09f906279650000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + invalidUtf8TopLevelKey: replacementString + }, + containsInvalid: true + }, + { + description: 'object with invalid utf8 in nested key', + buffer: Buffer.from( + '460000000276616c69644b657943686172000600000061626364650003746f704c766c4b6579001e00000002696e76616c69644b657900090000006869f09f90627965000000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + topLvlKey: { + invalidKey: replacementString + } + }, + containsInvalid: true + }, + { + description: 'object with invalid utf8 in nested key', + buffer: Buffer.from( + '5e0000000276616c69644b65794368617200040000006162630002696e76616c696455746638546f704c766c3100090000006869f09f906279650002696e76616c696455746638546f704c766c32000a000000f09f90f09f906279650000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abc', + invalidUtf8TopLvl1: replacementString, + invalidUtf8TopLvl2: twoCharReplacementStr + }, + containsInvalid: true } - }); + ]; - it('should disable UTF8 validation on any single key if validation option sets utf8: false', function () { - let validationOption = { validation: { utf8: false } }; - for (const example of invalidUtf8SingleKey) { - expect(() => BSON.deserialize(example, validationOption)).to.not.throw(); - } - }); + for (const { + description, + containsInvalid, + buffer, + expectedObjectWithReplacementChars + } of testInputs) { + const behavior = 'validate utf8 if no validation option given'; + it(`should ${behavior} for ${description}`, function () { + if (containsInvalid) { + expect(() => BSON.deserialize(buffer)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } else { + expect(BSON.deserialize(buffer)).to.deep.equals(expectedObjectWithReplacementChars); + } + }); + } - it('should enable UTF8 validation on any key if validation option sets utf8: true', function () { - let validationOption = { validation: { utf8: true } }; - for (const example of invalidUtf8SingleKey) { - expect(() => BSON.deserialize(example, validationOption)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' + for (const { description, buffer, expectedObjectWithReplacementChars } of testInputs) { + const behavior = 'not validate utf8 and not throw an error'; + it(`should ${behavior} for ${description} with global utf8 validation disabled`, function () { + const validation = { validation: { utf8: false } }; + expect(BSON.deserialize(buffer, validation)).to.deep.equals( + expectedObjectWithReplacementChars ); - } - }); - - const invalidUtf8ManyKeys = bufferFromHexArray([ - '02', // utf8 type - Buffer.from('validUtf8Chars', 'utf8').toString('hex') + '00', - stringToUTF8HexBytes('abc'), - '02', - Buffer.from('invalidUtf8', 'utf8').toString('hex') + '00', - '090000006869f09f9062796500', // value 'hi' + broken byte sequence + 'bye' - '02', - Buffer.from('invalidUtf82', 'utf8').toString('hex') + '00', - '0a000000f09f90f09f9062796500' // 2 broken byte sequences + 'bye' - ]); + }); + } - const expectedObjWithReplacements = { - validUtf8Chars: 'abc', - invalidUtf8: 'hi�bye', - invalidUtf82: '��bye' - }; + for (const { + description, + containsInvalid, + buffer, + expectedObjectWithReplacementChars + } of testInputs) { + const behavior = containsInvalid ? 'throw error' : 'validate utf8 with no errors'; + it(`should ${behavior} for ${description} with global utf8 validation enabled`, function () { + const validation = { validation: { utf8: true } }; + if (containsInvalid) { + expect(() => BSON.deserialize(buffer, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } else { + expect(BSON.deserialize(buffer, validation)).to.deep.equals( + expectedObjectWithReplacementChars + ); + } + }); + } - const testOutputObjects = [ + const utf8ValidationSpecifiedKeys = [ { - behavior: 'enable global UTF8 validation', - validation: { validation: { utf8: true } }, - errorExpect: true + validation: { validation: { utf8: { validKeyChar: false } } }, + behavior: + 'throw error when valid toplevel key has validation disabled but invalid toplevel key has validation enabled' }, { - behavior: 'globally disable UTF8 validation', - validation: { validation: { utf8: false } }, - errorExpect: false + validation: { validation: { utf8: { invalidUtf8TopLevelKey: false } } }, + behavior: + 'not throw when invalid toplevel key has validation disabled but valid toplevel key has validation enabled' }, { - behavior: 'enable UTF8 validation for specified key and disable for other keys', - validation: { validation: { utf8: { invalidUtf8: true } } }, - errorExpect: true + validation: { validation: { utf8: { validKeyChar: false, invalidUtf8TopLevelKey: false } } }, + behavior: 'not throw when both valid and invalid toplevel keys have validation disabled' }, { - behavior: 'disable UTF8 validation for specified key and enable for other keys', - validation: { validation: { utf8: { invalidUtf82: false } } }, - errorExpect: true + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: + 'not throw when valid toplevel key has validation enabled and invalid toplevel key has validation disabled' }, { - behavior: 'disable UTF8 validation for all specified keys', - validation: { validation: { utf8: { invalidUtf8: false, invalidUtf82: false } } }, - errorExpect: false + validation: { validation: { utf8: { invalidUtf8TopLevelKey: true } } }, + behavior: + 'throw error when invalid toplevel key has validation enabled but valid toplevel key has validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true, invalidUtf8TopLevelKey: true } } }, + behavior: 'throw error when both valid and invalid toplevel keys have validation enabled' } ]; - for (const { behavior, validation, errorExpect } of testOutputObjects) { - it(`should ${behavior} for object with invalid utf8 in top level keys`, function () { - if (isBrowser()) this.skip(); - const encodedObj = invalidUtf8ManyKeys; - if (errorExpect) { - expect(() => BSON.deserialize(encodedObj, validation)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' + for (const { behavior, validation } of utf8ValidationSpecifiedKeys) { + const topLvlKeysEx = testInputs[1]; + it(`should ${behavior}`, function () { + if (behavior.substring(0, 3) === 'not') { + expect(BSON.deserialize(topLvlKeysEx.buffer, validation)).to.deep.equals( + topLvlKeysEx.expectedObjectWithReplacementChars ); } else { - expect(BSON.deserialize(encodedObj, validation)).to.deep.equals( - expectedObjWithReplacements + expect(() => BSON.deserialize(topLvlKeysEx.buffer, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' ); } }); } - const invalidUtf8NestedKeys = bufferFromHexArray([ - '03' + Buffer.from('a', 'utf8').toString('hex') + '00', // key a - '3a000000', - '03' + Buffer.from('a1', 'utf8').toString('hex') + '00', // subkey a1 - '31000000', - '02' + Buffer.from('a11', 'utf8').toString('hex') + '00', // nested subkeys - stringToUTF8HexBytes('abcdefg'), - '02' + Buffer.from('invalidUtf81', 'utf8').toString('hex') + '00', - '090000006869f09f9062796500', - '00', - '00', - '03' + Buffer.from('b', 'utf8').toString('hex') + '00', // key b - '30000000', - '02' + Buffer.from('b1', 'utf8').toString('hex') + '00', // subkey b1 - stringToUTF8HexBytes('abcdefg'), - '02' + Buffer.from('invalidUtf82', 'utf8').toString('hex') + '00', // subkey invalidUtf82 - '090000006869f09f9062796500' + '00', - '02' + Buffer.from('invalidUtf83', 'utf8').toString('hex') + '00', // key invalidUtf83 - '090000006869f09f9062796500' - ]); - - const expectedNestedKeysObj = { - a: { - a1: { - a11: 'abcdefg', - invalidUtf81: 'hi�bye' - } + const utf8ValidationNestedInvalidKey = [ + { + validation: { validation: { utf8: { validKeyChar: false } } }, + behavior: + 'throw error when valid toplevel key has validation disabled but invalid nested key is validated' }, - b: { - b1: 'abcdefg', - invalidUtf82: 'hi�bye' + { + validation: { validation: { utf8: { topLvlKey: false } } }, + behavior: + 'not throw when toplevel key with invalid subkey has validation disabled but valid toplevel key is validated' }, - invalidUtf83: 'hi�bye' - }; - - const testOutputObjectsNested = [ { - behavior: 'enable global UTF8 validation', - validation: { validation: { utf8: true } }, - errorExpect: true + validation: { validation: { utf8: { invalidKey: false } } }, + behavior: + 'throw error when specified invalid key for disabling validation is not a top level key' }, { - behavior: 'globally disable UTF8 validation', - validation: { validation: { utf8: false } }, - errorExpect: false + validation: { validation: { utf8: { validKeyChar: false, topLvlKey: false } } }, + behavior: + 'not throw when both valid top level key and toplevel key with invalid subkey have validation disabled' }, { - behavior: 'disable UTF8 validation for specified key and enable for other keys', - validation: { validation: { utf8: { a: false } } }, - errorExpect: true + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: + 'not throw when valid toplevel key has validation enabled and invalid nested key is not validated' }, { - behavior: 'enable UTF8 validation for specified key and disable for other keys', - validation: { validation: { utf8: { a: true } } }, - errorExpect: true + validation: { validation: { utf8: { topLvlKey: true } } }, + behavior: + 'throw error when toplevel key containing nested invalid key has validation enabled but valid key is not validated' }, { - behavior: 'disable UTF8 validation on specified invalid keys', - validation: { validation: { utf8: { a: false, b: false } } }, - errorExpect: true + validation: { validation: { utf8: { validKeyChar: true, topLvlKey: true } } }, + behavior: + 'throw error when both valid key and nested invalid toplevel keys have validation enabled' + } + ]; + + for (const { behavior, validation } of utf8ValidationNestedInvalidKey) { + const nestedKeysEx = testInputs[2]; + it(`should ${behavior}`, function () { + if (behavior.substring(0, 3) === 'not') { + expect(BSON.deserialize(nestedKeysEx.buffer, validation)).to.deep.equals( + nestedKeysEx.expectedObjectWithReplacementChars + ); + } else { + expect(() => BSON.deserialize(nestedKeysEx.buffer, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } + }); + } + + const utf8ValidationMultipleInvalidKeys = [ + { + validation: { validation: { utf8: { invalidUtf8TopLvl1: false } } }, + behavior: 'throw error when only one of two invalid top level keys has validation disabled' }, { - behavior: 'disable UTF8 validation on all invalid keys', validation: { - validation: { - utf8: { a: false, b: false, invalidUtf83: false } - } + validation: { utf8: { invalidUtf8TopLvl1: false, invalidUtf8TopLvl2: false } } }, - errorExpect: false + behavior: 'not throw when all invalid top level keys have validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: 'not throw when only the valid top level key has enabled validation' + }, + { + validation: { validation: { utf8: { validKeyChar: true, invalidUtf8TopLvl1: true } } }, + behavior: + 'throw error when only the valid toplevel key and one of the invalid keys has enabled validation' } ]; - for (const { behavior, validation, errorExpect } of testOutputObjectsNested) { - it(`should ${behavior} for object with invalid utf8 in nested keys`, function () { - if (isBrowser()) this.skip(); - if (errorExpect) { - expect(() => BSON.deserialize(invalidUtf8NestedKeys, validation)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' + for (const { behavior, validation } of utf8ValidationMultipleInvalidKeys) { + const nestedKeysEx = testInputs[3]; + it(`should ${behavior}`, function () { + if (behavior.substring(0, 3) === 'not') { + expect(BSON.deserialize(nestedKeysEx.buffer, validation)).to.deep.equals( + nestedKeysEx.expectedObjectWithReplacementChars ); } else { - expect(BSON.deserialize(invalidUtf8NestedKeys, validation)).to.deep.equals( - expectedNestedKeysObj + expect(() => BSON.deserialize(nestedKeysEx.buffer, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' ); } }); From 5477c6419838de97de57e0d085663c46209140e2 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 12 Nov 2021 17:16:06 -0500 Subject: [PATCH 03/13] chore: remove .only --- test/node/utf8_tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index f5ec3d73..636d65e5 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -5,7 +5,7 @@ const BSON = require('../register-bson'); const { isBrowser } = require('./tools/utils'); const BSONError = BSON.BSONError; -describe.only('UTF8 validation', function () { +describe('UTF8 validation', function () { // Test both browser shims and node which have different replacement mechanisms const replacementChar = isBrowser() ? '���' : '�'; const replacementString = `hi${replacementChar}bye`; From adbdbdbc4b5d3b82d737b60f5ebd5a0df3d6486d Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Mon, 15 Nov 2021 11:18:53 -0500 Subject: [PATCH 04/13] test: add arrays to example objects --- test/node/utf8_tests.js | 42 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index 636d65e5..5a96418d 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -72,7 +72,7 @@ describe('UTF8 validation', function () { containsInvalid: true }, { - description: 'object with invalid utf8 in nested key', + description: 'object with invalid utf8 in nested key object', buffer: Buffer.from( '460000000276616c69644b657943686172000600000061626364650003746f704c766c4b6579001e00000002696e76616c69644b657900090000006869f09f90627965000000', 'hex' @@ -86,7 +86,7 @@ describe('UTF8 validation', function () { containsInvalid: true }, { - description: 'object with invalid utf8 in nested key', + description: 'object with invalid utf8 in two top level keys', buffer: Buffer.from( '5e0000000276616c69644b65794368617200040000006162630002696e76616c696455746638546f704c766c3100090000006869f09f906279650002696e76616c696455746638546f704c766c32000a000000f09f90f09f906279650000', 'hex' @@ -97,6 +97,44 @@ describe('UTF8 validation', function () { invalidUtf8TopLvl2: twoCharReplacementStr }, containsInvalid: true + }, + { + description: 'object with vakud utf8 in top level key array', + buffer: Buffer.from( + '4a0000000276616c69644b657943686172000600000061626364650004746f704c766c41727200220000000230000300000068690002310005000000f09f988e00103200393000000000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + topLvlArr: ['hi', '😎', 12345] + }, + containsInvalid: false + }, + { + description: 'object with invalid utf8 in top level key array', + buffer: Buffer.from( + '4e0000000276616c69644b657943686172000600000061626364650004746f704c766c417272002600000002300003000000686900023100090000006869f09f9062796500103200393000000000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + topLvlArr: ['hi', replacementString, 12345] + }, + containsInvalid: true + }, + { + description: 'object with invalid utf8 in nested key array', + buffer: Buffer.from( + '5a0000000276616c69644b657943686172000600000061626364650003746f704c766c4b65790032000000046e65737465644b6579417272001f00000002300003000000686900023100090000006869f09f9062796500000000', + 'hex' + ), + expectedObjectWithReplacementChars: { + validKeyChar: 'abcde', + topLvlKey: { + nestedKeyArr: ['hi', replacementString] + } + }, + containsInvalid: true } ]; From a4611fbe72bf33dc25cdb860843fd516ed105da6 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Mon, 15 Nov 2021 12:09:26 -0500 Subject: [PATCH 05/13] chore: fix evergreen error for unrecognized Object.values() --- src/parser/deserializer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index c7610fd4..0d7f1636 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -137,7 +137,9 @@ function deserializeObject( const keys = validation.utf8; if (typeof keys !== 'boolean') { globalUTFValidation = false; - const vals = Object.values(keys); + const vals = Object.keys(keys).map(function (key) { + return keys[key]; + }); if (vals.length !== 0) { // Ensures boolean uniformity in utf-8 validation (all true or all false) if (typeof vals[0] === 'boolean') { From 16ca4a6b33eec400e71232df0458b06c08b2dce6 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Mon, 15 Nov 2021 14:39:10 -0500 Subject: [PATCH 06/13] test: refactor tests for organization --- src/parser/deserializer.ts | 2 +- test/node/utf8_tests.js | 301 +++++++++++++++++++------------------ 2 files changed, 158 insertions(+), 145 deletions(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 0d7f1636..ef2d2ee1 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -324,7 +324,7 @@ function deserializeObject( arrayOptions['raw'] = true; } - value = deserializeObject(buffer, _index, arrayOptions, true); + value = deserializeObject(buffer, _index, arrayOptions, true, keyValidate); index = index + objectSize; if (buffer[index - 1] !== 0) throw new BSONError('invalid array terminator byte'); diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index 5a96418d..764d3638 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -57,7 +57,8 @@ describe('UTF8 validation', function () { validKeyChar: 'abcde', validKeyNum: 12345 }, - containsInvalid: false + containsInvalid: false, + testCases: [] }, { description: 'object with invalid utf8 top level key', @@ -69,7 +70,38 @@ describe('UTF8 validation', function () { validKeyChar: 'abcde', invalidUtf8TopLevelKey: replacementString }, - containsInvalid: true + containsInvalid: true, + testCases: [ + { + validation: { validation: { utf8: { validKeyChar: false } } }, + behavior: 'throw error when only valid toplevel key has validation disabled' + }, + { + validation: { validation: { utf8: { invalidUtf8TopLevelKey: false } } }, + behavior: 'not throw error when only invalid toplevel key has validation disabled' + }, + { + validation: { + validation: { utf8: { validKeyChar: false, invalidUtf8TopLevelKey: false } } + }, + behavior: + 'not throw error when both valid and invalid toplevel keys have validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: 'not throw error when only valid toplevel key has validation enabled' + }, + { + validation: { validation: { utf8: { invalidUtf8TopLevelKey: true } } }, + behavior: 'throw error when only invalid toplevel key has validation enabled' + }, + { + validation: { + validation: { utf8: { validKeyChar: true, invalidUtf8TopLevelKey: true } } + }, + behavior: 'throw error when both valid and invalid toplevel keys have validation enabled' + } + ] }, { description: 'object with invalid utf8 in nested key object', @@ -83,7 +115,42 @@ describe('UTF8 validation', function () { invalidKey: replacementString } }, - containsInvalid: true + containsInvalid: true, + testCases: [ + { + validation: { validation: { utf8: { validKeyChar: false } } }, + behavior: 'throw error when only valid toplevel key has validation disabled' + }, + { + validation: { validation: { utf8: { topLvlKey: false } } }, + behavior: + 'not throw error when only toplevel key with invalid subkey has validation disabled' + }, + { + validation: { validation: { utf8: { invalidKey: false } } }, + behavior: + 'throw error when specified invalid key for disabling validation is not a toplevel key' + }, + { + validation: { validation: { utf8: { validKeyChar: false, topLvlKey: false } } }, + behavior: + 'not throw error when both valid toplevel key and toplevel key with invalid subkey have validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: 'not throw error when only valid toplevel key has validation enabled' + }, + { + validation: { validation: { utf8: { topLvlKey: true } } }, + behavior: + 'throw error when only toplevel key containing nested invalid key has validation enabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true, topLvlKey: true } } }, + behavior: + 'throw error when both valid key and nested invalid toplevel keys have validation enabled' + } + ] }, { description: 'object with invalid utf8 in two top level keys', @@ -96,10 +163,32 @@ describe('UTF8 validation', function () { invalidUtf8TopLvl1: replacementString, invalidUtf8TopLvl2: twoCharReplacementStr }, - containsInvalid: true + containsInvalid: true, + testCases: [ + { + validation: { validation: { utf8: { invalidUtf8TopLvl1: false } } }, + behavior: + 'throw error when only one of two invalid top level keys has validation disabled' + }, + { + validation: { + validation: { utf8: { invalidUtf8TopLvl1: false, invalidUtf8TopLvl2: false } } + }, + behavior: 'not throw error when all invalid top level keys have validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true } } }, + behavior: 'not throw error when only the valid top level key has enabled validation' + }, + { + validation: { validation: { utf8: { validKeyChar: true, invalidUtf8TopLvl1: true } } }, + behavior: + 'throw error when only the valid toplevel key and one of the invalid keys has enabled validation' + } + ] }, { - description: 'object with vakud utf8 in top level key array', + description: 'object with valid utf8 in top level key array', buffer: Buffer.from( '4a0000000276616c69644b657943686172000600000061626364650004746f704c766c41727200220000000230000300000068690002310005000000f09f988e00103200393000000000', 'hex' @@ -108,7 +197,17 @@ describe('UTF8 validation', function () { validKeyChar: 'abcde', topLvlArr: ['hi', '😎', 12345] }, - containsInvalid: false + containsInvalid: false, + testCases: [ + { + validation: { validation: { utf8: { validKeyChar: false, topLvlArr: false } } }, + behavior: 'not throw error when both valid top level keys have validation disabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true, topLvlArr: true } } }, + behavior: 'not throw error when both valid top level keys have validation enabled' + } + ] }, { description: 'object with invalid utf8 in top level key array', @@ -120,7 +219,21 @@ describe('UTF8 validation', function () { validKeyChar: 'abcde', topLvlArr: ['hi', replacementString, 12345] }, - containsInvalid: true + containsInvalid: true, + testCases: [ + { + validation: { validation: { utf8: { topLvlArr: false } } }, + behavior: 'not throw error when invalid toplevel key array has validation disabled' + }, + { + validation: { validation: { utf8: { topLvlArr: true } } }, + behavior: 'throw error when invalid toplevel key array has validation enabled' + }, + { + validation: { validation: { utf8: { validKeyChar: true, topLvlArr: true } } }, + behavior: 'throw error when both valid and invalid toplevel keys have validation enabled' + } + ] }, { description: 'object with invalid utf8 in nested key array', @@ -134,7 +247,29 @@ describe('UTF8 validation', function () { nestedKeyArr: ['hi', replacementString] } }, - containsInvalid: true + containsInvalid: true, + testCases: [ + { + validation: { validation: { utf8: { topLvlKey: false } } }, + behavior: + 'not throw error when toplevel key for array with invalid key has validation disabled' + }, + { + validation: { validation: { utf8: { topLvlKey: true } } }, + behavior: + 'throw error when toplevel key for array with invalid key has validation enabled' + }, + { + validation: { validation: { utf8: { nestedKeyArr: false } } }, + behavior: + 'throw error when specified invalid key for disabling validation is not a toplevel key' + }, + { + validation: { validation: { utf8: { validKeyChar: true, topLvlKey: true } } }, + behavior: + 'throw error when both toplevel key and key with nested key with invalid array have validation enabled' + } + ] } ]; @@ -189,142 +324,20 @@ describe('UTF8 validation', function () { }); } - const utf8ValidationSpecifiedKeys = [ - { - validation: { validation: { utf8: { validKeyChar: false } } }, - behavior: - 'throw error when valid toplevel key has validation disabled but invalid toplevel key has validation enabled' - }, - { - validation: { validation: { utf8: { invalidUtf8TopLevelKey: false } } }, - behavior: - 'not throw when invalid toplevel key has validation disabled but valid toplevel key has validation enabled' - }, - { - validation: { validation: { utf8: { validKeyChar: false, invalidUtf8TopLevelKey: false } } }, - behavior: 'not throw when both valid and invalid toplevel keys have validation disabled' - }, - { - validation: { validation: { utf8: { validKeyChar: true } } }, - behavior: - 'not throw when valid toplevel key has validation enabled and invalid toplevel key has validation disabled' - }, - { - validation: { validation: { utf8: { invalidUtf8TopLevelKey: true } } }, - behavior: - 'throw error when invalid toplevel key has validation enabled but valid toplevel key has validation disabled' - }, - { - validation: { validation: { utf8: { validKeyChar: true, invalidUtf8TopLevelKey: true } } }, - behavior: 'throw error when both valid and invalid toplevel keys have validation enabled' - } - ]; - - for (const { behavior, validation } of utf8ValidationSpecifiedKeys) { - const topLvlKeysEx = testInputs[1]; - it(`should ${behavior}`, function () { - if (behavior.substring(0, 3) === 'not') { - expect(BSON.deserialize(topLvlKeysEx.buffer, validation)).to.deep.equals( - topLvlKeysEx.expectedObjectWithReplacementChars - ); - } else { - expect(() => BSON.deserialize(topLvlKeysEx.buffer, validation)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' - ); - } - }); - } - - const utf8ValidationNestedInvalidKey = [ - { - validation: { validation: { utf8: { validKeyChar: false } } }, - behavior: - 'throw error when valid toplevel key has validation disabled but invalid nested key is validated' - }, - { - validation: { validation: { utf8: { topLvlKey: false } } }, - behavior: - 'not throw when toplevel key with invalid subkey has validation disabled but valid toplevel key is validated' - }, - { - validation: { validation: { utf8: { invalidKey: false } } }, - behavior: - 'throw error when specified invalid key for disabling validation is not a top level key' - }, - { - validation: { validation: { utf8: { validKeyChar: false, topLvlKey: false } } }, - behavior: - 'not throw when both valid top level key and toplevel key with invalid subkey have validation disabled' - }, - { - validation: { validation: { utf8: { validKeyChar: true } } }, - behavior: - 'not throw when valid toplevel key has validation enabled and invalid nested key is not validated' - }, - { - validation: { validation: { utf8: { topLvlKey: true } } }, - behavior: - 'throw error when toplevel key containing nested invalid key has validation enabled but valid key is not validated' - }, - { - validation: { validation: { utf8: { validKeyChar: true, topLvlKey: true } } }, - behavior: - 'throw error when both valid key and nested invalid toplevel keys have validation enabled' - } - ]; - - for (const { behavior, validation } of utf8ValidationNestedInvalidKey) { - const nestedKeysEx = testInputs[2]; - it(`should ${behavior}`, function () { - if (behavior.substring(0, 3) === 'not') { - expect(BSON.deserialize(nestedKeysEx.buffer, validation)).to.deep.equals( - nestedKeysEx.expectedObjectWithReplacementChars - ); - } else { - expect(() => BSON.deserialize(nestedKeysEx.buffer, validation)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' - ); - } - }); - } - - const utf8ValidationMultipleInvalidKeys = [ - { - validation: { validation: { utf8: { invalidUtf8TopLvl1: false } } }, - behavior: 'throw error when only one of two invalid top level keys has validation disabled' - }, - { - validation: { - validation: { utf8: { invalidUtf8TopLvl1: false, invalidUtf8TopLvl2: false } } - }, - behavior: 'not throw when all invalid top level keys have validation disabled' - }, - { - validation: { validation: { utf8: { validKeyChar: true } } }, - behavior: 'not throw when only the valid top level key has enabled validation' - }, - { - validation: { validation: { utf8: { validKeyChar: true, invalidUtf8TopLvl1: true } } }, - behavior: - 'throw error when only the valid toplevel key and one of the invalid keys has enabled validation' + for (const { description, buffer, expectedObjectWithReplacementChars, testCases } of testInputs) { + for (const { behavior, validation } of testCases) { + it(`should ${behavior} for ${description}`, function () { + if (behavior.substring(0, 3) === 'not') { + expect(BSON.deserialize(buffer, validation)).to.deep.equals( + expectedObjectWithReplacementChars + ); + } else { + expect(() => BSON.deserialize(buffer, validation)).to.throw( + BSONError, + 'Invalid UTF-8 string in BSON document' + ); + } + }); } - ]; - - for (const { behavior, validation } of utf8ValidationMultipleInvalidKeys) { - const nestedKeysEx = testInputs[3]; - it(`should ${behavior}`, function () { - if (behavior.substring(0, 3) === 'not') { - expect(BSON.deserialize(nestedKeysEx.buffer, validation)).to.deep.equals( - nestedKeysEx.expectedObjectWithReplacementChars - ); - } else { - expect(() => BSON.deserialize(nestedKeysEx.buffer, validation)).to.throw( - BSONError, - 'Invalid UTF-8 string in BSON document' - ); - } - }); } }); From c7dff89e96971b0fbd6d5640da2ba9498ee3b7ad Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Mon, 15 Nov 2021 14:53:15 -0500 Subject: [PATCH 07/13] chore: fix evergreen error by checking for node6 --- test/node/tools/utils.js | 5 +++++ test/node/utf8_tests.js | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/node/tools/utils.js b/test/node/tools/utils.js index 5a0bb704..f990bbfb 100644 --- a/test/node/tools/utils.js +++ b/test/node/tools/utils.js @@ -150,3 +150,8 @@ exports.isBrowser = function () { // eslint-disable-next-line no-undef return typeof window === 'object' && typeof window['navigator'] === 'object'; }; + +exports.isNode6 = function () { + // eslint-disable-next-line no-undef + return process.version.split('.')[0] === 'v6'; +}; diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index 764d3638..519f726a 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -2,12 +2,12 @@ const { Buffer } = require('buffer'); const BSON = require('../register-bson'); -const { isBrowser } = require('./tools/utils'); +const { isNode6, isBrowser } = require('./tools/utils'); const BSONError = BSON.BSONError; describe('UTF8 validation', function () { // Test both browser shims and node which have different replacement mechanisms - const replacementChar = isBrowser() ? '���' : '�'; + const replacementChar = isNode6() || isBrowser() ? '���' : '�'; const replacementString = `hi${replacementChar}bye`; const twoCharReplacementStr = `${replacementChar}${replacementChar}bye`; const sampleValidUTF8 = BSON.serialize({ From 43ecaec66c613d7491eebb8737832203e659b43b Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Wed, 17 Nov 2021 13:17:53 -0500 Subject: [PATCH 08/13] chore: make pr changes and fix naming for clarity --- package-lock.json | 2 +- src/parser/deserializer.ts | 26 ++++++++++++++------------ test/node/tools/utils.js | 1 + test/node/utf8_tests.js | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1a8508c..21f1809a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "bson", - "version": "4.5.3", + "version": "4.5.4", "license": "Apache-2.0", "dependencies": { "buffer": "^5.6.0" diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index ef2d2ee1..572e019d 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -46,7 +46,7 @@ export interface DeserializeOptions { raw?: boolean; /** allows for opt-in utf-8 validation */ - validation?: Document; + validation?: { utf8: Record }; } // Internal long versions @@ -124,7 +124,7 @@ function deserializeObject( const promoteValues = options['promoteValues'] == null ? true : options['promoteValues']; // Ensures default validation option if none given - const validation = options['validation'] == null ? { utf8: true } : options['validation']; + const validation = options.validation == null ? { utf8: true } : options.validation; // Shows if global utf8 validation is enabled or disabled let globalUTFValidation = true; @@ -134,17 +134,17 @@ function deserializeObject( const utf8KeysSet = new Set(); // Check for boolean uniformity and empty validation option - const keys = validation.utf8; - if (typeof keys !== 'boolean') { + const utf8ValidatedKeys = validation.utf8; + if (typeof utf8ValidatedKeys !== 'boolean') { globalUTFValidation = false; - const vals = Object.keys(keys).map(function (key) { - return keys[key]; + const utf8ValidationValues = Object.keys(utf8ValidatedKeys).map(function (key) { + return utf8ValidatedKeys[key]; }); - if (vals.length !== 0) { + if (utf8ValidationValues.length !== 0) { // Ensures boolean uniformity in utf-8 validation (all true or all false) - if (typeof vals[0] === 'boolean') { - uniformBool = vals[0]; - if (!vals.every(item => item === uniformBool)) { + if (typeof utf8ValidationValues[0] === 'boolean') { + uniformBool = utf8ValidationValues[0]; + if (!utf8ValidationValues.every(item => item === uniformBool)) { throw new BSONError( 'Invalid UTF-8 validation option - keys must be all true or all false' ); @@ -156,12 +156,14 @@ function deserializeObject( throw new BSONError('validation option is empty'); } } else { - uniformBool = keys; + uniformBool = utf8ValidatedKeys; } // Add keys to set that will either be validated or not based on uniformBool if ((!uniformBool && !globalUTFValidation) || (uniformBool && !globalUTFValidation)) { - Object.keys(keys).forEach(key => utf8KeysSet.add(key)); + for (const key of Object.keys(utf8ValidatedKeys)) { + utf8KeysSet.add(key); + } } // Set the start index diff --git a/test/node/tools/utils.js b/test/node/tools/utils.js index f990bbfb..ae464833 100644 --- a/test/node/tools/utils.js +++ b/test/node/tools/utils.js @@ -141,6 +141,7 @@ const stringToUTF8HexBytes = str => { var out = Buffer.alloc(len + 4 + 1); out.writeInt32LE(len + 1, 0); out.set(b, 4); + out[len + 1] = 0x00; return out.toString('hex'); }; diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index 519f726a..cf461190 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -7,7 +7,7 @@ const BSONError = BSON.BSONError; describe('UTF8 validation', function () { // Test both browser shims and node which have different replacement mechanisms - const replacementChar = isNode6() || isBrowser() ? '���' : '�'; + const replacementChar = isNode6() || isBrowser() ? '\u{FFFD}\u{FFFD}\u{FFFD}' : '\u{FFFD}'; const replacementString = `hi${replacementChar}bye`; const twoCharReplacementStr = `${replacementChar}${replacementChar}bye`; const sampleValidUTF8 = BSON.serialize({ From 18e90484dde7a03386b01ea00f866695216f83d8 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Wed, 17 Nov 2021 19:32:11 -0500 Subject: [PATCH 09/13] chore: add documentation and fix naming --- src/parser/deserializer.ts | 39 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 572e019d..c7e92aa9 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -45,8 +45,19 @@ export interface DeserializeOptions { index?: number; raw?: boolean; - /** allows for opt-in utf-8 validation */ - validation?: { utf8: Record }; + /** allows for opt-out utf-8 validation for all keys or + * specified keys. Must be all true or all false. + * + * @example + * ```js + * // disables validation on all keys + * validation: { utf8: false } + * + * // enables validation on specified keys a, b, and c + * validation: { utf8: { a: true, b: true, c: true } } + * ``` + */ + validation?: { utf8: boolean | Record }; } // Internal long versions @@ -126,10 +137,10 @@ function deserializeObject( // Ensures default validation option if none given const validation = options.validation == null ? { utf8: true } : options.validation; - // Shows if global utf8 validation is enabled or disabled + // Shows if global utf-8 validation is enabled or disabled let globalUTFValidation = true; - // Reflects utf8 validation boolean regardless of global or specific key validation - let uniformBool: boolean; + // Reflects utf-8 validation setting regardless of global or specific key validation + let validationSetting: boolean; // Set of keys either to enable or disable validation on const utf8KeysSet = new Set(); @@ -143,8 +154,8 @@ function deserializeObject( if (utf8ValidationValues.length !== 0) { // Ensures boolean uniformity in utf-8 validation (all true or all false) if (typeof utf8ValidationValues[0] === 'boolean') { - uniformBool = utf8ValidationValues[0]; - if (!utf8ValidationValues.every(item => item === uniformBool)) { + validationSetting = utf8ValidationValues[0]; + if (!utf8ValidationValues.every(item => item === validationSetting)) { throw new BSONError( 'Invalid UTF-8 validation option - keys must be all true or all false' ); @@ -156,11 +167,11 @@ function deserializeObject( throw new BSONError('validation option is empty'); } } else { - uniformBool = utf8ValidatedKeys; + validationSetting = utf8ValidatedKeys; } - // Add keys to set that will either be validated or not based on uniformBool - if ((!uniformBool && !globalUTFValidation) || (uniformBool && !globalUTFValidation)) { + // Add keys to set that will either be validated or not based on validationSetting + if ((!validationSetting && !globalUTFValidation) || (validationSetting && !globalUTFValidation)) { for (const key of Object.keys(utf8ValidatedKeys)) { utf8KeysSet.add(key); } @@ -211,12 +222,12 @@ function deserializeObject( // keyValidate is true if the key should be validated, false otherwise let keyValidate = true; if (globalUTFValidation) { - keyValidate = uniformBool; + keyValidate = validationSetting; } else { if (utf8KeysSet.has(name)) { - keyValidate = uniformBool; - } else if (!utf8KeysSet.has(name)) { - keyValidate = !uniformBool; + keyValidate = validationSetting; + } else { + keyValidate = !validationSetting; } } // if nested key, validate based on top level key From f1ad08029999aa831aa3308104391ba709b082b8 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 19 Nov 2021 11:20:58 -0500 Subject: [PATCH 10/13] refactor: add test coverage, clean up and remove unnecessary code --- package.json | 2 +- src/parser/deserializer.ts | 101 ++++++++++++++++--------------------- test/node/utf8_tests.js | 31 ++++++++++-- 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 71ce3ec7..f2f6ce68 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "test": "npm run build && npm run test-node && npm run test-browser", "test-node": "mocha test/node test/*_tests.js", "test-tsd": "npm run build:dts && tsd", - "test-browser": "karma start karma.conf.js", + "test-browser": "node --max-old-space-size=4096 ./node_modules/.bin/karma start karma.conf.js", "build:ts": "tsc", "build:dts": "npm run build:ts && api-extractor run --typescript-compiler-folder node_modules/typescript --local && rimraf 'lib/**/*.d.ts*' && downlevel-dts bson.d.ts bson.d.ts", "build:bundle": "rollup -c rollup.config.js", diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index c7e92aa9..28c1289b 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -45,7 +45,7 @@ export interface DeserializeOptions { index?: number; raw?: boolean; - /** allows for opt-out utf-8 validation for all keys or + /** Allows for opt-out utf-8 validation for all keys or * specified keys. Must be all true or all false. * * @example @@ -53,11 +53,14 @@ export interface DeserializeOptions { * // disables validation on all keys * validation: { utf8: false } * - * // enables validation on specified keys a, b, and c + * // enables validation only on specified keys a, b, and c * validation: { utf8: { a: true, b: true, c: true } } + * + * // disables validation only on specified keys a, b + * validation: { utf8: { a: false, b: false } } * ``` */ - validation?: { utf8: boolean | Record }; + validation?: { utf8: boolean | Record | Record }; } // Internal long versions @@ -115,8 +118,7 @@ function deserializeObject( buffer: Buffer, index: number, options: DeserializeOptions, - isArray = false, - nestedKey?: boolean + isArray = false ) { const evalFunctions = options['evalFunctions'] == null ? false : options['evalFunctions']; const cacheFunctions = options['cacheFunctions'] == null ? false : options['cacheFunctions']; @@ -146,32 +148,31 @@ function deserializeObject( // Check for boolean uniformity and empty validation option const utf8ValidatedKeys = validation.utf8; - if (typeof utf8ValidatedKeys !== 'boolean') { + if (typeof utf8ValidatedKeys === 'boolean') { + validationSetting = utf8ValidatedKeys; + } else { globalUTFValidation = false; const utf8ValidationValues = Object.keys(utf8ValidatedKeys).map(function (key) { return utf8ValidatedKeys[key]; }); - if (utf8ValidationValues.length !== 0) { - // Ensures boolean uniformity in utf-8 validation (all true or all false) - if (typeof utf8ValidationValues[0] === 'boolean') { - validationSetting = utf8ValidationValues[0]; - if (!utf8ValidationValues.every(item => item === validationSetting)) { - throw new BSONError( - 'Invalid UTF-8 validation option - keys must be all true or all false' - ); - } - } else { - throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); - } - } else { - throw new BSONError('validation option is empty'); + if (utf8ValidationValues.length === 0) { + throw new BSONError('UTF-8 validation setting cannot be empty'); + } + // if (typeof utf8ValidationValues[0] !== 'boolean') { + // throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); + // } + if (!utf8ValidationValues.every(item => typeof item === 'boolean')) { + throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); + } + validationSetting = utf8ValidationValues[0]; + // Ensures boolean uniformity in utf-8 validation (all true or all false) + if (!utf8ValidationValues.every(item => item === validationSetting)) { + throw new BSONError('Invalid UTF-8 validation option - keys must be all true or all false'); } - } else { - validationSetting = utf8ValidatedKeys; } // Add keys to set that will either be validated or not based on validationSetting - if ((!validationSetting && !globalUTFValidation) || (validationSetting && !globalUTFValidation)) { + if (!globalUTFValidation) { for (const key of Object.keys(utf8ValidatedKeys)) { utf8KeysSet.add(key); } @@ -219,21 +220,17 @@ function deserializeObject( // Represents the key const name = isArray ? arrayIndex++ : buffer.toString('utf8', index, i); - // keyValidate is true if the key should be validated, false otherwise - let keyValidate = true; + // shouldValidateKey is true if the key should be validated, false otherwise + let shouldValidateKey = true; if (globalUTFValidation) { - keyValidate = validationSetting; + shouldValidateKey = validationSetting; } else { if (utf8KeysSet.has(name)) { - keyValidate = validationSetting; + shouldValidateKey = validationSetting; } else { - keyValidate = !validationSetting; + shouldValidateKey = !validationSetting; } } - // if nested key, validate based on top level key - if (nestedKey != null) { - keyValidate = nestedKey; - } if (isPossibleDBRef !== false && (name as string)[0] === '$') { isPossibleDBRef = allowedDBRefKeys.test(name as string); @@ -255,7 +252,7 @@ function deserializeObject( ) { throw new BSONError('bad string length in bson'); } - value = getValidatedString(buffer, index, index + stringSize - 1, validation, keyValidate); + value = getValidatedString(buffer, index, index + stringSize - 1, shouldValidateKey); index = index + stringSize; } else if (elementType === constants.BSON_DATA_OID) { const oid = Buffer.alloc(12); @@ -308,7 +305,8 @@ function deserializeObject( if (raw) { value = buffer.slice(index, index + objectSize); } else { - value = deserializeObject(buffer, _index, options, false, keyValidate); + options.validation = { utf8: shouldValidateKey }; + value = deserializeObject(buffer, _index, options, false); } index = index + objectSize; @@ -336,8 +334,8 @@ function deserializeObject( } arrayOptions['raw'] = true; } - - value = deserializeObject(buffer, _index, arrayOptions, true, keyValidate); + arrayOptions.validation = { utf8: shouldValidateKey }; + value = deserializeObject(buffer, _index, arrayOptions, true); index = index + objectSize; if (buffer[index - 1] !== 0) throw new BSONError('invalid array terminator byte'); @@ -537,13 +535,7 @@ function deserializeObject( ) { throw new BSONError('bad string length in bson'); } - const symbol = getValidatedString( - buffer, - index, - index + stringSize - 1, - validation, - keyValidate - ); + const symbol = getValidatedString(buffer, index, index + stringSize - 1, shouldValidateKey); value = promoteValues ? symbol : new BSONSymbol(symbol); index = index + stringSize; } else if (elementType === constants.BSON_DATA_TIMESTAMP) { @@ -580,8 +572,7 @@ function deserializeObject( buffer, index, index + stringSize - 1, - validation, - keyValidate + shouldValidateKey ); // If we are evaluating the functions @@ -631,8 +622,7 @@ function deserializeObject( buffer, index, index + stringSize - 1, - validation, - keyValidate + shouldValidateKey ); // Update parse index position index = index + stringSize; @@ -768,20 +758,17 @@ function getValidatedString( buffer: Buffer, start: number, end: number, - validation: Document, - check: boolean + shouldValidateUtf8: boolean ) { const value = buffer.toString('utf8', start, end); // if utf8 validation is on, do the check - if (check) { - if (validation.utf8 != null && validation.utf8) { - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) === 0xfffd) { - if (!validateUtf8(buffer, start, end)) { - throw new BSONError('Invalid UTF-8 string in BSON document'); - } - break; + if (shouldValidateUtf8) { + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) === 0xfffd) { + if (!validateUtf8(buffer, start, end)) { + throw new BSONError('Invalid UTF-8 string in BSON document'); } + break; } } } diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index cf461190..ad4a9bb2 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -30,19 +30,40 @@ describe('UTF8 validation', function () { }); it('should correctly handle validation if validation option contains all T or all F with valid utf8 example', function () { - let allTrue = { validation: { utf8: { a: true, b: true, c: true } } }; - let allFalse = { validation: { utf8: { a: false, b: false, c: false, d: false } } }; + const allTrue = { validation: { utf8: { a: true, b: true, c: true } } }; + const allFalse = { validation: { utf8: { a: false, b: false, c: false, d: false } } }; expect(() => BSON.deserialize(sampleValidUTF8, allTrue)).to.not.throw(); expect(() => BSON.deserialize(sampleValidUTF8, allFalse)).to.not.throw(); }); it('should throw error if empty utf8 validation option passed in', function () { - var doc = { a: 'validation utf8 option cant be empty' }; + const doc = { a: 'validation utf8 option cant be empty' }; const serialized = BSON.serialize(doc); - let emptyUTF8validation = { validation: { utf8: {} } }; + const emptyUTF8validation = { validation: { utf8: {} } }; expect(() => BSON.deserialize(serialized, emptyUTF8validation)).to.throw( BSONError, - 'validation option is empty' + 'UTF-8 validation setting cannot be empty' + ); + }); + + it('should throw error if non-boolean utf8 field for validation option is specified for a key', function () { + const utf8InvalidOptionObj = { validation: { utf8: { a: { a: true } } } }; + const utf8InvalidOptionArr = { + validation: { utf8: { a: ['should', 'be', 'boolean'], b: true } } + }; + const utf8InvalidOptionStr = { validation: { utf8: { a: 'bad value', b: true } } }; + + expect(() => BSON.deserialize(sampleValidUTF8, utf8InvalidOptionObj)).to.throw( + BSONError, + 'Invalid UTF-8 validation option, must specify boolean values' + ); + expect(() => BSON.deserialize(sampleValidUTF8, utf8InvalidOptionArr)).to.throw( + BSONError, + 'Invalid UTF-8 validation option, must specify boolean values' + ); + expect(() => BSON.deserialize(sampleValidUTF8, utf8InvalidOptionStr)).to.throw( + BSONError, + 'Invalid UTF-8 validation option, must specify boolean values' ); }); From 5b525ff420457f0fe8678c073950ab4e70d57130 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 19 Nov 2021 12:16:55 -0500 Subject: [PATCH 11/13] test: add type tests for updated type definition --- test/types/deserialize.test-d.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/types/deserialize.test-d.ts diff --git a/test/types/deserialize.test-d.ts b/test/types/deserialize.test-d.ts new file mode 100644 index 00000000..4d5d3020 --- /dev/null +++ b/test/types/deserialize.test-d.ts @@ -0,0 +1,17 @@ +import { expectType, expectError } from 'tsd'; +import { deserialize, serialize } from '../../bson'; + +const sampleValidUTF8 = serialize({ + a: '😎', + b: 'valid utf8', + c: 12345 +}); + +expectError(deserialize(sampleValidUTF8, { validation: { utf8: { a: false, b: true } } })); +expectError(deserialize(sampleValidUTF8, { validation: { utf8: { a: true, b: true, c: false } } })); + +// all true and all false validation utf8 options are valid +deserialize(sampleValidUTF8, { validation: { utf8: { a: true, b: true, c: true } } }); +deserialize(sampleValidUTF8, { validation: { utf8: { a: false, b: false, c: false} } }); +deserialize(sampleValidUTF8, { validation: { utf8: true } }); +deserialize(sampleValidUTF8, { validation: { utf8: true } }); From ba9bde349227bc27302dc9969fc9d039f0f815ec Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 19 Nov 2021 17:32:28 -0500 Subject: [PATCH 12/13] chore: address pr comments and clean up code --- src/parser/deserializer.ts | 24 +++++++++++------------- test/node/utf8_tests.js | 6 ++++-- test/types/deserialize.test-d.ts | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 28c1289b..f0589ce1 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -158,10 +158,7 @@ function deserializeObject( if (utf8ValidationValues.length === 0) { throw new BSONError('UTF-8 validation setting cannot be empty'); } - // if (typeof utf8ValidationValues[0] !== 'boolean') { - // throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); - // } - if (!utf8ValidationValues.every(item => typeof item === 'boolean')) { + if (typeof utf8ValidationValues[0] !== 'boolean') { throw new BSONError('Invalid UTF-8 validation option, must specify boolean values'); } validationSetting = utf8ValidationValues[0]; @@ -222,14 +219,10 @@ function deserializeObject( // shouldValidateKey is true if the key should be validated, false otherwise let shouldValidateKey = true; - if (globalUTFValidation) { + if (globalUTFValidation || utf8KeysSet.has(name)) { shouldValidateKey = validationSetting; } else { - if (utf8KeysSet.has(name)) { - shouldValidateKey = validationSetting; - } else { - shouldValidateKey = !validationSetting; - } + shouldValidateKey = !validationSetting; } if (isPossibleDBRef !== false && (name as string)[0] === '$') { @@ -305,8 +298,11 @@ function deserializeObject( if (raw) { value = buffer.slice(index, index + objectSize); } else { - options.validation = { utf8: shouldValidateKey }; - value = deserializeObject(buffer, _index, options, false); + let objectOptions = options; + if (!globalUTFValidation) { + objectOptions = { ...options, validation: { utf8: shouldValidateKey } }; + } + value = deserializeObject(buffer, _index, objectOptions, false); } index = index + objectSize; @@ -334,7 +330,9 @@ function deserializeObject( } arrayOptions['raw'] = true; } - arrayOptions.validation = { utf8: shouldValidateKey }; + if (!globalUTFValidation) { + arrayOptions = { ...options, validation: { utf8: shouldValidateKey } }; + } value = deserializeObject(buffer, _index, arrayOptions, true); index = index + objectSize; diff --git a/test/node/utf8_tests.js b/test/node/utf8_tests.js index ad4a9bb2..7cb55877 100644 --- a/test/node/utf8_tests.js +++ b/test/node/utf8_tests.js @@ -316,7 +316,7 @@ describe('UTF8 validation', function () { for (const { description, buffer, expectedObjectWithReplacementChars } of testInputs) { const behavior = 'not validate utf8 and not throw an error'; it(`should ${behavior} for ${description} with global utf8 validation disabled`, function () { - const validation = { validation: { utf8: false } }; + const validation = Object.freeze({ validation: Object.freeze({ utf8: false }) }); expect(BSON.deserialize(buffer, validation)).to.deep.equals( expectedObjectWithReplacementChars ); @@ -331,7 +331,7 @@ describe('UTF8 validation', function () { } of testInputs) { const behavior = containsInvalid ? 'throw error' : 'validate utf8 with no errors'; it(`should ${behavior} for ${description} with global utf8 validation enabled`, function () { - const validation = { validation: { utf8: true } }; + const validation = Object.freeze({ validation: Object.freeze({ utf8: true }) }); if (containsInvalid) { expect(() => BSON.deserialize(buffer, validation)).to.throw( BSONError, @@ -348,6 +348,8 @@ describe('UTF8 validation', function () { for (const { description, buffer, expectedObjectWithReplacementChars, testCases } of testInputs) { for (const { behavior, validation } of testCases) { it(`should ${behavior} for ${description}`, function () { + Object.freeze(validation); + Object.freeze(validation.utf8); if (behavior.substring(0, 3) === 'not') { expect(BSON.deserialize(buffer, validation)).to.deep.equals( expectedObjectWithReplacementChars diff --git a/test/types/deserialize.test-d.ts b/test/types/deserialize.test-d.ts index 4d5d3020..a1f3edb0 100644 --- a/test/types/deserialize.test-d.ts +++ b/test/types/deserialize.test-d.ts @@ -14,4 +14,4 @@ expectError(deserialize(sampleValidUTF8, { validation: { utf8: { a: true, b: tru deserialize(sampleValidUTF8, { validation: { utf8: { a: true, b: true, c: true } } }); deserialize(sampleValidUTF8, { validation: { utf8: { a: false, b: false, c: false} } }); deserialize(sampleValidUTF8, { validation: { utf8: true } }); -deserialize(sampleValidUTF8, { validation: { utf8: true } }); +deserialize(sampleValidUTF8, { validation: { utf8: false } }); From 805b8b9816d66c408a2d7a0b2bd7c83da5387316 Mon Sep 17 00:00:00 2001 From: Grace Chong Date: Fri, 19 Nov 2021 17:53:31 -0500 Subject: [PATCH 13/13] chore: fix option passing --- src/parser/deserializer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index f0589ce1..411731e7 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -331,7 +331,7 @@ function deserializeObject( arrayOptions['raw'] = true; } if (!globalUTFValidation) { - arrayOptions = { ...options, validation: { utf8: shouldValidateKey } }; + arrayOptions = { ...arrayOptions, validation: { utf8: shouldValidateKey } }; } value = deserializeObject(buffer, _index, arrayOptions, true); index = index + objectSize;