Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(NODE-1921)!: validate serializer root input #537

Merged
merged 9 commits into from
Dec 13, 2022
21 changes: 21 additions & 0 deletions docs/upgrade-to-v5.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,
durran marked this conversation as resolved.
Show resolved Hide resolved
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') {
durran marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Loading