Skip to content

Commit

Permalink
feat(NODE-1921)!: validate serializer root input (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Dec 13, 2022
1 parent 0427eb5 commit 95d5edf
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 141 deletions.
21 changes: 21 additions & 0 deletions docs/upgrade-to-v5.md
Expand Up @@ -217,3 +217,24 @@ iLoveJavascript();
// prints "I love javascript"
// iLoveJavascript.name === "iLoveJavascript"
```

### `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' }
```
5 changes: 3 additions & 2 deletions src/bson.ts
Expand Up @@ -109,7 +109,7 @@ export function serialize(object: Document, options: SerializeOptions = {}): Uin
0,
serializeFunctions,
ignoreUndefined,
[]
null
);

// Create the final buffer
Expand Down Expand Up @@ -152,7 +152,8 @@ export function serializeWithBufferAndIndex(
0,
0,
serializeFunctions,
ignoreUndefined
ignoreUndefined,
null
);

finalBuffer.set(buffer.subarray(0, serializationIndex), startIndex);
Expand Down
119 changes: 86 additions & 33 deletions src/parser/serializer.ts
Expand Up @@ -13,7 +13,15 @@ import type { MinKey } from '../min_key';
import type { ObjectId } from '../objectid';
import type { BSONRegExp } from '../regexp';
import { ByteUtils } from '../utils/byte_utils';
import { isBigInt64Array, isBigUInt64Array, isDate, isMap, isRegExp, isUint8Array } from './utils';
import {
isAnyArrayBuffer,
isBigInt64Array,
isBigUInt64Array,
isDate,
isMap,
isRegExp,
isUint8Array
} from './utils';

/** @public */
export interface SerializeOptions {
Expand Down Expand Up @@ -270,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<Document>
) {
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
Expand All @@ -299,8 +307,9 @@ function serializeObject(
ignoreUndefined,
path
);
// Pop stack
path.pop();

path.delete(value);

return endIndex;
}

Expand Down Expand Up @@ -410,7 +419,8 @@ function serializeCode(
checkKeys = false,
depth = 0,
serializeFunctions = false,
ignoreUndefined = true
ignoreUndefined = true,
path: Set<Document>
) {
if (value.scope && typeof value.scope === 'object') {
// Write the type
Expand Down Expand Up @@ -441,7 +451,6 @@ function serializeCode(
// Write the
index = index + codeSize + 4;

//
// Serialize the scope value
const endIndex = serializeInto(
buffer,
Expand All @@ -450,7 +459,8 @@ function serializeCode(
index,
depth + 1,
serializeFunctions,
ignoreUndefined
ignoreUndefined,
path
);
index = endIndex - 1;

Expand Down Expand Up @@ -555,7 +565,8 @@ function serializeDBRef(
value: DBRef,
index: number,
depth: number,
serializeFunctions: boolean
serializeFunctions: boolean,
path: Set<Document>
) {
// Write the type
buffer[index++] = constants.BSON_DATA_OBJECT;
Expand All @@ -577,7 +588,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;
Expand All @@ -593,18 +613,48 @@ 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<Document> | 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');
} 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();
}

// Push the object to the path
path.push(object);
path.add(object);

// Start place to serialize into
let index = startingIndex + 4;
Expand Down Expand Up @@ -674,14 +724,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') {
Expand Down Expand Up @@ -772,7 +823,8 @@ export function serializeInto(
checkKeys,
depth,
serializeFunctions,
ignoreUndefined
ignoreUndefined,
path
);
} else if (typeof value === 'function' && serializeFunctions) {
index = serializeFunction(buffer, key, value, index);
Expand All @@ -781,7 +833,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') {
Expand Down Expand Up @@ -876,7 +928,8 @@ export function serializeInto(
checkKeys,
depth,
serializeFunctions,
ignoreUndefined
ignoreUndefined,
path
);
} else if (typeof value === 'function' && serializeFunctions) {
index = serializeFunction(buffer, key, value, index);
Expand All @@ -885,7 +938,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') {
Expand All @@ -899,7 +952,7 @@ export function serializeInto(
}

// Remove the path
path.pop();
path.delete(object);

// Final padding byte for object
buffer[index++] = 0x00;
Expand Down
4 changes: 3 additions & 1 deletion test/node/bson_test.js
Expand Up @@ -1849,7 +1849,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]
);
Expand Down

0 comments on commit 95d5edf

Please sign in to comment.