From e0e877f499f3f004f17037a74232809285a592a7 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 7 Dec 2022 14:34:19 -0500 Subject: [PATCH 1/7] feat!(NODE-1921): validate serializer root input --- docs/upgrade-to-v5.md | 21 ++++++ src/bson.ts | 5 +- src/parser/serializer.ts | 100 +++++++++++++++++-------- test/node/bson_test.js | 4 +- test/node/circular_reference.test.ts | 106 +++++++++++++++++++++++++++ test/node/detect_cyclic_dep_tests.js | 57 -------------- test/node/extended_json.test.ts | 48 ------------ test/node/parser/serializer.test.ts | 45 ++++++++++++ 8 files changed, 246 insertions(+), 140 deletions(-) create mode 100644 test/node/circular_reference.test.ts delete mode 100644 test/node/detect_cyclic_dep_tests.js diff --git a/docs/upgrade-to-v5.md b/docs/upgrade-to-v5.md index 9cf7a7016..8d300da13 100644 --- a/docs/upgrade-to-v5.md +++ b/docs/upgrade-to-v5.md @@ -130,3 +130,24 @@ Now `-0` can be used directly BSON.deserialize(BSON.serialize({ d: -0 })) // type preservation, returns { d: -0 } ``` + +### `BSON.serialize()` validation + +The BSON format does not support encoding arrays as the **root** object. +However, in javascript arrays are just objects where the keys are numeric (and a magic `length` property), so round tripping an array (ex. `[1, 2]`) though BSON would return `{ '0': 1, '1': 2 }`. + +`BSON.serialize()` now validates input types, the input to serialize must be an object or a `Map`, arrays will now cause an error. + +```typescript +BSON.serialize([1, 2, 3]) +// BSONError: serialize does not support an array as the root input +``` + +if the functionality of turning arrays into an object with numeric keys is useful see the following example: + +```typescript +// Migration example: +const result = BSON.serialize(Object.fromEntries([1, true, 'blue'].entries())) +BSON.deserialize(result) +// { '0': 1, '1': true, '2': 'blue' } +``` diff --git a/src/bson.ts b/src/bson.ts index fa1d5bbbf..90ab2bf81 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -150,7 +150,7 @@ export function serialize(object: Document, options: SerializeOptions = {}): Uin 0, serializeFunctions, ignoreUndefined, - [] + null ); // Create the final buffer @@ -193,7 +193,8 @@ export function serializeWithBufferAndIndex( 0, 0, serializeFunctions, - ignoreUndefined + ignoreUndefined, + null ); finalBuffer.set(buffer.subarray(0, serializationIndex), startIndex); diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index e9232ce7c..89132e19f 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -278,18 +278,18 @@ function serializeObject( key: string, value: Document, index: number, - checkKeys = false, - depth = 0, - serializeFunctions = false, - ignoreUndefined = true, - path: Document[] = [] + checkKeys: boolean, + depth: number, + serializeFunctions: boolean, + ignoreUndefined: boolean, + path: Set ) { - for (let i = 0; i < path.length; i++) { - if (path[i] === value) throw new BSONError('cyclic dependency detected'); + if (path.has(value)) { + throw new BSONError('Cannot convert circular structure to BSON'); } - // Push value to stack - path.push(value); + path.add(value); + // Write the type buffer[index++] = Array.isArray(value) ? constants.BSON_DATA_ARRAY : constants.BSON_DATA_OBJECT; // Number of written bytes @@ -307,8 +307,9 @@ function serializeObject( ignoreUndefined, path ); - // Pop stack - path.pop(); + + path.delete(value); + return endIndex; } @@ -425,7 +426,8 @@ function serializeCode( checkKeys = false, depth = 0, serializeFunctions = false, - ignoreUndefined = true + ignoreUndefined = true, + path: Set ) { if (value.scope && typeof value.scope === 'object') { // Write the type @@ -456,7 +458,6 @@ function serializeCode( // Write the index = index + codeSize + 4; - // // Serialize the scope value const endIndex = serializeInto( buffer, @@ -465,7 +466,8 @@ function serializeCode( index, depth + 1, serializeFunctions, - ignoreUndefined + ignoreUndefined, + path ); index = endIndex - 1; @@ -570,7 +572,8 @@ function serializeDBRef( value: DBRef, index: number, depth: number, - serializeFunctions: boolean + serializeFunctions: boolean, + path: Set ) { // Write the type buffer[index++] = constants.BSON_DATA_OBJECT; @@ -592,7 +595,16 @@ function serializeDBRef( } output = Object.assign(output, value.fields); - const endIndex = serializeInto(buffer, output, false, index, depth + 1, serializeFunctions); + const endIndex = serializeInto( + buffer, + output, + false, + index, + depth + 1, + serializeFunctions, + true, + path + ); // Calculate object size const size = endIndex - startIndex; @@ -608,18 +620,39 @@ function serializeDBRef( export function serializeInto( buffer: Uint8Array, object: Document, - checkKeys = false, - startingIndex = 0, - depth = 0, - serializeFunctions = false, - ignoreUndefined = true, - path: Document[] = [] + checkKeys: boolean, + startingIndex: number, + depth: number, + serializeFunctions: boolean, + ignoreUndefined: boolean, + path: Set | null ): number { - startingIndex = startingIndex || 0; - path = path || []; + if (path == null) { + // We are at the root input + if (object == null) { + // ONLY the root should turn into an empty document + // BSON Empty document has a size of 5 (LE) + buffer[0] = 0x05; + buffer[1] = 0x00; + buffer[2] = 0x00; + buffer[3] = 0x00; + // All documents end with null terminator + buffer[4] = 0x00; + return 5; + } + + if (Array.isArray(object)) { + throw new BSONError('serialize does not support an array as the root input'); + } + if (typeof object !== 'object') { + throw new BSONError('serialize does not support non-object as the root input'); + } + + path = new Set(); + } // Push the object to the path - path.push(object); + path.add(object); // Start place to serialize into let index = startingIndex + 4; @@ -689,14 +722,15 @@ export function serializeInto( checkKeys, depth, serializeFunctions, - ignoreUndefined + ignoreUndefined, + path ); } else if (value['_bsontype'] === 'Binary') { index = serializeBinary(buffer, key, value, index); } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index); } else if (value['_bsontype'] === 'DBRef') { - index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions); + index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); } else if (value['_bsontype'] === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); } else if (value['_bsontype'] === 'Int32') { @@ -787,7 +821,8 @@ export function serializeInto( checkKeys, depth, serializeFunctions, - ignoreUndefined + ignoreUndefined, + path ); } else if (typeof value === 'function' && serializeFunctions) { index = serializeFunction(buffer, key, value, index, checkKeys, depth); @@ -796,7 +831,7 @@ export function serializeInto( } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index); } else if (value['_bsontype'] === 'DBRef') { - index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions); + index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); } else if (value['_bsontype'] === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); } else if (value['_bsontype'] === 'Int32') { @@ -891,7 +926,8 @@ export function serializeInto( checkKeys, depth, serializeFunctions, - ignoreUndefined + ignoreUndefined, + path ); } else if (typeof value === 'function' && serializeFunctions) { index = serializeFunction(buffer, key, value, index, checkKeys, depth); @@ -900,7 +936,7 @@ export function serializeInto( } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index); } else if (value['_bsontype'] === 'DBRef') { - index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions); + index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); } else if (value['_bsontype'] === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); } else if (value['_bsontype'] === 'Int32') { @@ -914,7 +950,7 @@ export function serializeInto( } // Remove the path - path.pop(); + path.delete(object); // Final padding byte for object buffer[index++] = 0x00; diff --git a/test/node/bson_test.js b/test/node/bson_test.js index c0c314cb4..2eb6a1b32 100644 --- a/test/node/bson_test.js +++ b/test/node/bson_test.js @@ -1848,7 +1848,9 @@ describe('BSON', function () { // Array const array = [new ObjectIdv400(), new OldObjectID(), new ObjectId()]; - const deserializedArrayAsMap = BSON.deserialize(BSON.serialize(array)); + const deserializedArrayAsMap = BSON.deserialize( + BSON.serialize(Object.fromEntries(array.entries())) + ); const deserializedArray = Object.keys(deserializedArrayAsMap).map( x => deserializedArrayAsMap[x] ); diff --git a/test/node/circular_reference.test.ts b/test/node/circular_reference.test.ts new file mode 100644 index 000000000..e6be34acd --- /dev/null +++ b/test/node/circular_reference.test.ts @@ -0,0 +1,106 @@ +import { expect } from 'chai'; +import * as BSON from '../register-bson'; +import type { Document } from '../..'; +import { inspect } from 'node:util'; +import { isMap } from 'node:util/types'; + +const EJSON = BSON.EJSON; + +function setOn(object: Document | unknown[] | Map, value: unknown) { + if (Array.isArray(object)) { + object[Math.floor(Math.random() * 250)] = value; + } else if (isMap(object)) { + // @ts-expect-error: "readonly" map case does not apply + object.set('a', value); + } else { + object.a = value; + } +} + +function* generateTests() { + const levelsOfDepth = 25; + for (let lvl = 2; lvl < levelsOfDepth; lvl++) { + const isRootMap = Math.random() < 0.5; + const root = isRootMap ? new Map() : {}; + + let lastReference = root; + for (let depth = 1; depth < lvl; depth++) { + const referenceChoice = Math.random(); + const newLevel = + referenceChoice < 0.3 + ? {} + : referenceChoice > 0.3 && referenceChoice < 0.6 + ? [] + : new Map(); + + setOn(lastReference, newLevel); + lastReference = newLevel; + } + + // Add the cycle + setOn(lastReference, root); + + yield { + title: `cyclic reference nested ${lvl} levels will cause the serializer to throw`, + input: root + }; + } +} + +describe('Cyclic reference detection', () => { + context('BSON circular references', () => { + for (const test of generateTests()) { + it(test.title, () => { + expect(() => BSON.serialize(test.input), inspect(test.input)).to.throw(/circular/); + }); + } + }); + + context('EJSON circular references', () => { + it('should throw a helpful error message for input with circular references', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = { + some: { + property: { + array: [] + } + } + }; + obj.some.property.array.push(obj.some); + expect(() => EJSON.serialize(obj)).to.throw(`\ +Converting circular structure to EJSON: + (root) -> some -> property -> array -> index 0 + \\-----------------------------/`); + }); + + it('should throw a helpful error message for input with circular references, one-level nested', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = {}; + obj.obj = obj; + expect(() => EJSON.serialize(obj)).to.throw(`\ +Converting circular structure to EJSON: + (root) -> obj + \\-------/`); + }); + + it('should throw a helpful error message for input with circular references, one-level nested inside base object', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = {}; + obj.obj = obj; + expect(() => EJSON.serialize({ foo: obj })).to.throw(`\ +Converting circular structure to EJSON: + (root) -> foo -> obj + \\------/`); + }); + + it('should throw a helpful error message for input with circular references, pointing back to base object', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const obj: any = { foo: {} }; + obj.foo.obj = obj; + expect(() => EJSON.serialize(obj)).to.throw(`\ +Converting circular structure to EJSON: + (root) -> foo -> obj + \\--------------/`); + }); + }); +}); diff --git a/test/node/detect_cyclic_dep_tests.js b/test/node/detect_cyclic_dep_tests.js deleted file mode 100644 index 0c7342aa2..000000000 --- a/test/node/detect_cyclic_dep_tests.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const BSON = require('../register-bson'); - -describe('Cyclic Dependencies', function () { - /** - * @ignore - */ - it('Should correctly detect cyclic dependency in nested objects', function (done) { - // Force cyclic dependency - var a = { b: {} }; - a.b.c = a; - try { - // Attempt to serialize cyclic dependency - BSON.serialize(a); - } catch (err) { - expect('cyclic dependency detected').to.equal(err.message); - } - - done(); - }); - - /** - * @ignore - */ - it('Should correctly detect cyclic dependency in deeploy nested objects', function (done) { - // Force cyclic dependency - var a = { b: { c: [{ d: {} }] } }; - a.b.c[0].d.a = a; - - try { - // Attempt to serialize cyclic dependency - BSON.serialize(a); - } catch (err) { - expect('cyclic dependency detected').to.equal(err.message); - } - - done(); - }); - - /** - * @ignore - */ - it('Should correctly detect cyclic dependency in nested array', function (done) { - // Force cyclic dependency - var a = { b: {} }; - a.b.c = [a]; - try { - // Attempt to serialize cyclic dependency - BSON.serialize(a); - } catch (err) { - expect('cyclic dependency detected').to.equal(err.message); - } - - done(); - }); -}); diff --git a/test/node/extended_json.test.ts b/test/node/extended_json.test.ts index c84ed08d4..c65051002 100644 --- a/test/node/extended_json.test.ts +++ b/test/node/extended_json.test.ts @@ -528,54 +528,6 @@ describe('Extended JSON', function () { expect(deserialized.__proto__.a).to.equal(42); }); - context('circular references', () => { - it('should throw a helpful error message for input with circular references', function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = { - some: { - property: { - array: [] - } - } - }; - obj.some.property.array.push(obj.some); - expect(() => EJSON.serialize(obj)).to.throw(`\ -Converting circular structure to EJSON: - (root) -> some -> property -> array -> index 0 - \\-----------------------------/`); - }); - - it('should throw a helpful error message for input with circular references, one-level nested', function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = {}; - obj.obj = obj; - expect(() => EJSON.serialize(obj)).to.throw(`\ -Converting circular structure to EJSON: - (root) -> obj - \\-------/`); - }); - - it('should throw a helpful error message for input with circular references, one-level nested inside base object', function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = {}; - obj.obj = obj; - expect(() => EJSON.serialize({ foo: obj })).to.throw(`\ -Converting circular structure to EJSON: - (root) -> foo -> obj - \\------/`); - }); - - it('should throw a helpful error message for input with circular references, pointing back to base object', function () { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const obj: any = { foo: {} }; - obj.foo.obj = obj; - expect(() => EJSON.serialize(obj)).to.throw(`\ -Converting circular structure to EJSON: - (root) -> foo -> obj - \\--------------/`); - }); - }); - context('when dealing with legacy extended json', function () { describe('.stringify', function () { context('when serializing binary', function () { diff --git a/test/node/parser/serializer.test.ts b/test/node/parser/serializer.test.ts index 10b839ed5..5718fcc42 100644 --- a/test/node/parser/serializer.test.ts +++ b/test/node/parser/serializer.test.ts @@ -14,4 +14,49 @@ describe('serialize()', () => { ]) ); }); + + it('returns an empty document when the root input is nullish', () => { + // @ts-expect-error: Testing nullish input, not supported by type defs but code works gracefully + const emptyDocumentUndef = BSON.serialize(undefined); + expect(emptyDocumentUndef).to.deep.equal(new Uint8Array([5, 0, 0, 0, 0])); + // @ts-expect-error: Testing nullish input, not supported by type defs but code works gracefully + const emptyDocumentNull = BSON.serialize(null); + expect(emptyDocumentNull).to.deep.equal(new Uint8Array([5, 0, 0, 0, 0])); + }); + + it('does not turn nested nulls into empty documents', () => { + const nestedNull = bufferFromHexArray([ + '0A', // null type + '6100', // 'a\x00' + '' // null is encoded as nothing + ]); + const emptyDocumentUndef = BSON.serialize({ a: undefined }, { ignoreUndefined: false }); + expect(emptyDocumentUndef).to.deep.equal(nestedNull); + const emptyDocumentNull = BSON.serialize({ a: null }); + expect(emptyDocumentNull).to.deep.equal(nestedNull); + }); + + describe('validates input types', () => { + it('does not permit arrays as the root input', () => { + expect(() => BSON.serialize([])).to.throw(/does not support an array/); + }); + + it('does not non-objects as the root input', () => { + // @ts-expect-error: Testing invalid input + expect(() => BSON.serialize(true)).to.throw(/does not support non-object/); + // @ts-expect-error: Testing invalid input + expect(() => BSON.serialize(2)).to.throw(/does not support non-object/); + // @ts-expect-error: Testing invalid input + expect(() => BSON.serialize(2n)).to.throw(/does not support non-object/); + // @ts-expect-error: Testing invalid input + expect(() => BSON.serialize(Symbol())).to.throw(/does not support non-object/); + // @ts-expect-error: Testing invalid input + expect(() => BSON.serialize('')).to.throw(/does not support non-object/); + expect(() => + BSON.serialize(function () { + // ignore + }) + ).to.throw(/does not support non-object/); + }); + }); }); From 3634d435d051003d65f0f6d5590b08524b166cac Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 7 Dec 2022 14:53:33 -0500 Subject: [PATCH 2/7] test: add code and dbref --- test/node/circular_reference.test.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/test/node/circular_reference.test.ts b/test/node/circular_reference.test.ts index e6be34acd..1b65e003d 100644 --- a/test/node/circular_reference.test.ts +++ b/test/node/circular_reference.test.ts @@ -1,14 +1,15 @@ import { expect } from 'chai'; import * as BSON from '../register-bson'; -import type { Document } from '../..'; +import type { Code, Document } from '../..'; import { inspect } from 'node:util'; import { isMap } from 'node:util/types'; +import { DBRef } from '../register-bson'; const EJSON = BSON.EJSON; function setOn(object: Document | unknown[] | Map, value: unknown) { if (Array.isArray(object)) { - object[Math.floor(Math.random() * 250)] = value; + object[Math.floor(Math.random() * object.length)] = value; } else if (isMap(object)) { // @ts-expect-error: "readonly" map case does not apply object.set('a', value); @@ -18,6 +19,7 @@ function setOn(object: Document | unknown[] | Map, value: unkno } function* generateTests() { + // arbitrarily depth choice here... it could fail at 26! but is that worth testing? const levelsOfDepth = 25; for (let lvl = 2; lvl < levelsOfDepth; lvl++) { const isRootMap = Math.random() < 0.5; @@ -30,7 +32,8 @@ function* generateTests() { referenceChoice < 0.3 ? {} : referenceChoice > 0.3 && referenceChoice < 0.6 - ? [] + ? // Just making an arbitrarily largish non-sparse array here + Array.from({ length: Math.floor(Math.random() * 255) + 5 }, () => null) : new Map(); setOn(lastReference, newLevel); @@ -48,7 +51,7 @@ function* generateTests() { } describe('Cyclic reference detection', () => { - context('BSON circular references', () => { + context('fuzz BSON circular references', () => { for (const test of generateTests()) { it(test.title, () => { expect(() => BSON.serialize(test.input), inspect(test.input)).to.throw(/circular/); @@ -56,6 +59,22 @@ describe('Cyclic reference detection', () => { } }); + context('in Code with scope', () => { + it('throws if code.scope is circular', () => { + const root: { code: Code | null } = { code: null }; + root.code = new BSON.Code('function() {}', { a: root }); + expect(() => BSON.serialize(root)).to.throw(/circular/); + }); + }); + + context('in DBRef with fields', () => { + it('throws if dbref.fields is circular', () => { + const root: { dbref: DBRef | null } = { dbref: null }; + root.dbref = new BSON.DBRef('test', new BSON.ObjectId(), 'test', { a: root }); + expect(() => BSON.serialize(root)).to.throw(/circular/); + }); + }); + context('EJSON circular references', () => { it('should throw a helpful error message for input with circular references', function () { // eslint-disable-next-line @typescript-eslint/no-explicit-any From 23623d0b68cbffebc7689fecf84650ed8444792b Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 7 Dec 2022 14:55:54 -0500 Subject: [PATCH 3/7] test: fix node14 import --- test/node/circular_reference.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/node/circular_reference.test.ts b/test/node/circular_reference.test.ts index 1b65e003d..8210721d7 100644 --- a/test/node/circular_reference.test.ts +++ b/test/node/circular_reference.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import * as BSON from '../register-bson'; import type { Code, Document } from '../..'; -import { inspect } from 'node:util'; -import { isMap } from 'node:util/types'; +import { inspect, types } from 'node:util'; import { DBRef } from '../register-bson'; const EJSON = BSON.EJSON; @@ -10,7 +9,7 @@ const EJSON = BSON.EJSON; function setOn(object: Document | unknown[] | Map, value: unknown) { if (Array.isArray(object)) { object[Math.floor(Math.random() * object.length)] = value; - } else if (isMap(object)) { + } else if (types.isMap(object)) { // @ts-expect-error: "readonly" map case does not apply object.set('a', value); } else { From ab171e319784ae4f4c48bd60c833bea921a0d271 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Thu, 8 Dec 2022 10:45:23 -0500 Subject: [PATCH 4/7] test: make generation less fuzzy --- test/node/circular_reference.test.ts | 38 +++++++++------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/test/node/circular_reference.test.ts b/test/node/circular_reference.test.ts index 8210721d7..e70fe1920 100644 --- a/test/node/circular_reference.test.ts +++ b/test/node/circular_reference.test.ts @@ -18,34 +18,20 @@ function setOn(object: Document | unknown[] | Map, value: unkno } function* generateTests() { - // arbitrarily depth choice here... it could fail at 26! but is that worth testing? - const levelsOfDepth = 25; - for (let lvl = 2; lvl < levelsOfDepth; lvl++) { - const isRootMap = Math.random() < 0.5; - const root = isRootMap ? new Map() : {}; + for (const makeRoot of [() => new Map(), () => ({})]) { + for (const makeNestedType of [() => new Map(), () => ({}), () => []]) { + const root = makeRoot(); + const nested = makeNestedType(); + setOn(root, nested); + setOn(nested, root); - let lastReference = root; - for (let depth = 1; depth < lvl; depth++) { - const referenceChoice = Math.random(); - const newLevel = - referenceChoice < 0.3 - ? {} - : referenceChoice > 0.3 && referenceChoice < 0.6 - ? // Just making an arbitrarily largish non-sparse array here - Array.from({ length: Math.floor(Math.random() * 255) + 5 }, () => null) - : new Map(); - - setOn(lastReference, newLevel); - lastReference = newLevel; + yield { + title: `root that is a ${types.isMap(root) ? 'map' : 'object'} with a nested ${ + types.isMap(nested) ? 'map' : Array.isArray(nested) ? 'array' : 'object' + } with a circular reference to the root throws`, + input: root + }; } - - // Add the cycle - setOn(lastReference, root); - - yield { - title: `cyclic reference nested ${lvl} levels will cause the serializer to throw`, - input: root - }; } } From d691473ebb8e876417b678d6f25bd3c7bc0d877a Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 9 Dec 2022 11:18:24 -0500 Subject: [PATCH 5/7] fix: add gating for bson types at the top level --- src/parser/serializer.ts | 2 ++ test/node/parser/serializer.test.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 89132e19f..31d08bd30 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -646,6 +646,8 @@ export function serializeInto( } if (typeof object !== 'object') { throw new BSONError('serialize does not support non-object as the root input'); + } else if ('_bsontype' in object && typeof object._bsontype === 'string') { + throw new BSONError(`BSON types cannot be serialized as a document`); } path = new Set(); diff --git a/test/node/parser/serializer.test.ts b/test/node/parser/serializer.test.ts index 5718fcc42..d5327e190 100644 --- a/test/node/parser/serializer.test.ts +++ b/test/node/parser/serializer.test.ts @@ -41,7 +41,15 @@ describe('serialize()', () => { expect(() => BSON.serialize([])).to.throw(/does not support an array/); }); - it('does not non-objects as the root input', () => { + it('does not permit objects with a _bsontype string to be serialized at the root', () => { + expect(() => BSON.serialize({ _bsontype: 'iLoveJavascript' })).to.throw(/BSON types cannot/); + // a nested invalid _bsontype throws something different + expect(() => BSON.serialize({ a: { _bsontype: 'iLoveJavascript' } })).to.throw( + /invalid _bsontype/ + ); + }); + + it('does not permit non-objects as the root input', () => { // @ts-expect-error: Testing invalid input expect(() => BSON.serialize(true)).to.throw(/does not support non-object/); // @ts-expect-error: Testing invalid input From 2a5ed28fe7b69c45b7c77fef19b2e75f484ed0d4 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 12 Dec 2022 14:25:29 -0500 Subject: [PATCH 6/7] test: non-string _bsontype is allowed --- test/node/parser/serializer.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/node/parser/serializer.test.ts b/test/node/parser/serializer.test.ts index d5327e190..5200756ed 100644 --- a/test/node/parser/serializer.test.ts +++ b/test/node/parser/serializer.test.ts @@ -49,6 +49,20 @@ describe('serialize()', () => { ); }); + it('does permit objects with a _bsontype prop that is not a string', () => { + const expected = bufferFromHexArray([ + '10', // int32 + Buffer.from('_bsontype\x00', 'utf8').toString('hex'), + '02000000' + ]); + const result = BSON.serialize({ _bsontype: 2 }); + expect(result).to.deep.equal(expected); + + expect(() => BSON.serialize({ _bsontype: true })).to.not.throw(); + expect(() => BSON.serialize({ _bsontype: /a/ })).to.not.throw(); + expect(() => BSON.serialize({ _bsontype: new Date() })).to.not.throw(); + }); + it('does not permit non-objects as the root input', () => { // @ts-expect-error: Testing invalid input expect(() => BSON.serialize(true)).to.throw(/does not support non-object/); From 06f6bca089744364742e32081c47d20287139672 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Mon, 12 Dec 2022 14:31:18 -0500 Subject: [PATCH 7/7] fix: prevent js objects that are typically values --- src/parser/serializer.ts | 8 ++++++++ test/node/parser/serializer.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 31d08bd30..eded6cb07 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -14,6 +14,7 @@ import type { ObjectId } from '../objectid'; import type { BSONRegExp } from '../regexp'; import { ByteUtils } from '../utils/byte_utils'; import { + isAnyArrayBuffer, isBigInt64Array, isBigUInt64Array, isDate, @@ -648,6 +649,13 @@ export function serializeInto( throw new BSONError('serialize does not support non-object as the root input'); } else if ('_bsontype' in object && typeof object._bsontype === 'string') { throw new BSONError(`BSON types cannot be serialized as a document`); + } else if ( + isDate(object) || + isRegExp(object) || + isUint8Array(object) || + isAnyArrayBuffer(object) + ) { + throw new BSONError(`date, regexp, typedarray, and arraybuffer cannot be BSON documents`); } path = new Set(); diff --git a/test/node/parser/serializer.test.ts b/test/node/parser/serializer.test.ts index 5200756ed..352fc538d 100644 --- a/test/node/parser/serializer.test.ts +++ b/test/node/parser/serializer.test.ts @@ -80,5 +80,13 @@ describe('serialize()', () => { }) ).to.throw(/does not support non-object/); }); + + it('does not permit certain objects that are typically values as the root input', () => { + expect(() => BSON.serialize(new Date())).to.throw(/cannot be BSON documents/); + expect(() => BSON.serialize(/a/)).to.throw(/cannot be BSON documents/); + expect(() => BSON.serialize(new ArrayBuffer(2))).to.throw(/cannot be BSON documents/); + expect(() => BSON.serialize(Buffer.alloc(2))).to.throw(/cannot be BSON documents/); + expect(() => BSON.serialize(new Uint8Array(3))).to.throw(/cannot be BSON documents/); + }); }); });