Skip to content

Commit 95d5edf

Browse files
authored
feat(NODE-1921)!: validate serializer root input (#537)
1 parent 0427eb5 commit 95d5edf

File tree

8 files changed

+298
-141
lines changed

8 files changed

+298
-141
lines changed

docs/upgrade-to-v5.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,24 @@ iLoveJavascript();
217217
// prints "I love javascript"
218218
// iLoveJavascript.name === "iLoveJavascript"
219219
```
220+
221+
### `BSON.serialize()` validation
222+
223+
The BSON format does not support encoding arrays as the **root** object.
224+
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 }`.
225+
226+
`BSON.serialize()` now validates input types, the input to serialize must be an object or a `Map`, arrays will now cause an error.
227+
228+
```typescript
229+
BSON.serialize([1, 2, 3])
230+
// BSONError: serialize does not support an array as the root input
231+
```
232+
233+
if the functionality of turning arrays into an object with numeric keys is useful see the following example:
234+
235+
```typescript
236+
// Migration example:
237+
const result = BSON.serialize(Object.fromEntries([1, true, 'blue'].entries()))
238+
BSON.deserialize(result)
239+
// { '0': 1, '1': true, '2': 'blue' }
240+
```

src/bson.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function serialize(object: Document, options: SerializeOptions = {}): Uin
109109
0,
110110
serializeFunctions,
111111
ignoreUndefined,
112-
[]
112+
null
113113
);
114114

115115
// Create the final buffer
@@ -152,7 +152,8 @@ export function serializeWithBufferAndIndex(
152152
0,
153153
0,
154154
serializeFunctions,
155-
ignoreUndefined
155+
ignoreUndefined,
156+
null
156157
);
157158

158159
finalBuffer.set(buffer.subarray(0, serializationIndex), startIndex);

src/parser/serializer.ts

Lines changed: 86 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ import type { MinKey } from '../min_key';
1313
import type { ObjectId } from '../objectid';
1414
import type { BSONRegExp } from '../regexp';
1515
import { ByteUtils } from '../utils/byte_utils';
16-
import { isBigInt64Array, isBigUInt64Array, isDate, isMap, isRegExp, isUint8Array } from './utils';
16+
import {
17+
isAnyArrayBuffer,
18+
isBigInt64Array,
19+
isBigUInt64Array,
20+
isDate,
21+
isMap,
22+
isRegExp,
23+
isUint8Array
24+
} from './utils';
1725

1826
/** @public */
1927
export interface SerializeOptions {
@@ -270,18 +278,18 @@ function serializeObject(
270278
key: string,
271279
value: Document,
272280
index: number,
273-
checkKeys = false,
274-
depth = 0,
275-
serializeFunctions = false,
276-
ignoreUndefined = true,
277-
path: Document[] = []
281+
checkKeys: boolean,
282+
depth: number,
283+
serializeFunctions: boolean,
284+
ignoreUndefined: boolean,
285+
path: Set<Document>
278286
) {
279-
for (let i = 0; i < path.length; i++) {
280-
if (path[i] === value) throw new BSONError('cyclic dependency detected');
287+
if (path.has(value)) {
288+
throw new BSONError('Cannot convert circular structure to BSON');
281289
}
282290

283-
// Push value to stack
284-
path.push(value);
291+
path.add(value);
292+
285293
// Write the type
286294
buffer[index++] = Array.isArray(value) ? constants.BSON_DATA_ARRAY : constants.BSON_DATA_OBJECT;
287295
// Number of written bytes
@@ -299,8 +307,9 @@ function serializeObject(
299307
ignoreUndefined,
300308
path
301309
);
302-
// Pop stack
303-
path.pop();
310+
311+
path.delete(value);
312+
304313
return endIndex;
305314
}
306315

@@ -410,7 +419,8 @@ function serializeCode(
410419
checkKeys = false,
411420
depth = 0,
412421
serializeFunctions = false,
413-
ignoreUndefined = true
422+
ignoreUndefined = true,
423+
path: Set<Document>
414424
) {
415425
if (value.scope && typeof value.scope === 'object') {
416426
// Write the type
@@ -441,7 +451,6 @@ function serializeCode(
441451
// Write the
442452
index = index + codeSize + 4;
443453

444-
//
445454
// Serialize the scope value
446455
const endIndex = serializeInto(
447456
buffer,
@@ -450,7 +459,8 @@ function serializeCode(
450459
index,
451460
depth + 1,
452461
serializeFunctions,
453-
ignoreUndefined
462+
ignoreUndefined,
463+
path
454464
);
455465
index = endIndex - 1;
456466

@@ -555,7 +565,8 @@ function serializeDBRef(
555565
value: DBRef,
556566
index: number,
557567
depth: number,
558-
serializeFunctions: boolean
568+
serializeFunctions: boolean,
569+
path: Set<Document>
559570
) {
560571
// Write the type
561572
buffer[index++] = constants.BSON_DATA_OBJECT;
@@ -577,7 +588,16 @@ function serializeDBRef(
577588
}
578589

579590
output = Object.assign(output, value.fields);
580-
const endIndex = serializeInto(buffer, output, false, index, depth + 1, serializeFunctions);
591+
const endIndex = serializeInto(
592+
buffer,
593+
output,
594+
false,
595+
index,
596+
depth + 1,
597+
serializeFunctions,
598+
true,
599+
path
600+
);
581601

582602
// Calculate object size
583603
const size = endIndex - startIndex;
@@ -593,18 +613,48 @@ function serializeDBRef(
593613
export function serializeInto(
594614
buffer: Uint8Array,
595615
object: Document,
596-
checkKeys = false,
597-
startingIndex = 0,
598-
depth = 0,
599-
serializeFunctions = false,
600-
ignoreUndefined = true,
601-
path: Document[] = []
616+
checkKeys: boolean,
617+
startingIndex: number,
618+
depth: number,
619+
serializeFunctions: boolean,
620+
ignoreUndefined: boolean,
621+
path: Set<Document> | null
602622
): number {
603-
startingIndex = startingIndex || 0;
604-
path = path || [];
623+
if (path == null) {
624+
// We are at the root input
625+
if (object == null) {
626+
// ONLY the root should turn into an empty document
627+
// BSON Empty document has a size of 5 (LE)
628+
buffer[0] = 0x05;
629+
buffer[1] = 0x00;
630+
buffer[2] = 0x00;
631+
buffer[3] = 0x00;
632+
// All documents end with null terminator
633+
buffer[4] = 0x00;
634+
return 5;
635+
}
636+
637+
if (Array.isArray(object)) {
638+
throw new BSONError('serialize does not support an array as the root input');
639+
}
640+
if (typeof object !== 'object') {
641+
throw new BSONError('serialize does not support non-object as the root input');
642+
} else if ('_bsontype' in object && typeof object._bsontype === 'string') {
643+
throw new BSONError(`BSON types cannot be serialized as a document`);
644+
} else if (
645+
isDate(object) ||
646+
isRegExp(object) ||
647+
isUint8Array(object) ||
648+
isAnyArrayBuffer(object)
649+
) {
650+
throw new BSONError(`date, regexp, typedarray, and arraybuffer cannot be BSON documents`);
651+
}
652+
653+
path = new Set();
654+
}
605655

606656
// Push the object to the path
607-
path.push(object);
657+
path.add(object);
608658

609659
// Start place to serialize into
610660
let index = startingIndex + 4;
@@ -674,14 +724,15 @@ export function serializeInto(
674724
checkKeys,
675725
depth,
676726
serializeFunctions,
677-
ignoreUndefined
727+
ignoreUndefined,
728+
path
678729
);
679730
} else if (value['_bsontype'] === 'Binary') {
680731
index = serializeBinary(buffer, key, value, index);
681732
} else if (value['_bsontype'] === 'Symbol') {
682733
index = serializeSymbol(buffer, key, value, index);
683734
} else if (value['_bsontype'] === 'DBRef') {
684-
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions);
735+
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path);
685736
} else if (value['_bsontype'] === 'BSONRegExp') {
686737
index = serializeBSONRegExp(buffer, key, value, index);
687738
} else if (value['_bsontype'] === 'Int32') {
@@ -772,7 +823,8 @@ export function serializeInto(
772823
checkKeys,
773824
depth,
774825
serializeFunctions,
775-
ignoreUndefined
826+
ignoreUndefined,
827+
path
776828
);
777829
} else if (typeof value === 'function' && serializeFunctions) {
778830
index = serializeFunction(buffer, key, value, index);
@@ -781,7 +833,7 @@ export function serializeInto(
781833
} else if (value['_bsontype'] === 'Symbol') {
782834
index = serializeSymbol(buffer, key, value, index);
783835
} else if (value['_bsontype'] === 'DBRef') {
784-
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions);
836+
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path);
785837
} else if (value['_bsontype'] === 'BSONRegExp') {
786838
index = serializeBSONRegExp(buffer, key, value, index);
787839
} else if (value['_bsontype'] === 'Int32') {
@@ -876,7 +928,8 @@ export function serializeInto(
876928
checkKeys,
877929
depth,
878930
serializeFunctions,
879-
ignoreUndefined
931+
ignoreUndefined,
932+
path
880933
);
881934
} else if (typeof value === 'function' && serializeFunctions) {
882935
index = serializeFunction(buffer, key, value, index);
@@ -885,7 +938,7 @@ export function serializeInto(
885938
} else if (value['_bsontype'] === 'Symbol') {
886939
index = serializeSymbol(buffer, key, value, index);
887940
} else if (value['_bsontype'] === 'DBRef') {
888-
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions);
941+
index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path);
889942
} else if (value['_bsontype'] === 'BSONRegExp') {
890943
index = serializeBSONRegExp(buffer, key, value, index);
891944
} else if (value['_bsontype'] === 'Int32') {
@@ -899,7 +952,7 @@ export function serializeInto(
899952
}
900953

901954
// Remove the path
902-
path.pop();
955+
path.delete(object);
903956

904957
// Final padding byte for object
905958
buffer[index++] = 0x00;

test/node/bson_test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1849,7 +1849,9 @@ describe('BSON', function () {
18491849

18501850
// Array
18511851
const array = [new ObjectIdv400(), new OldObjectID(), new ObjectId()];
1852-
const deserializedArrayAsMap = BSON.deserialize(BSON.serialize(array));
1852+
const deserializedArrayAsMap = BSON.deserialize(
1853+
BSON.serialize(Object.fromEntries(array.entries()))
1854+
);
18531855
const deserializedArray = Object.keys(deserializedArrayAsMap).map(
18541856
x => deserializedArrayAsMap[x]
18551857
);

0 commit comments

Comments
 (0)