Skip to content

Commit 5103e4d

Browse files
feat!(NODE-4410): only enumerate own properties (#527)
Co-authored-by: Bailey Pearson <[email protected]>
1 parent be74b30 commit 5103e4d

File tree

9 files changed

+103
-37
lines changed

9 files changed

+103
-37
lines changed

docs/upgrade-to-v5.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ The following deprecated methods have been removed:
101101
- `ObjectId.get_inc`
102102
- The `static getInc()` is private since invoking it increments the next `ObjectId` index, so invoking would impact the creation of subsequent ObjectIds.
103103

104+
### BSON Element names are now fetched only from object's own properties
105+
106+
`BSON.serialize`, `EJSON.stringify` and `BSON.calculateObjectSize` now only inspect own properties and do not consider properties defined on the prototype of the input.
107+
108+
```typescript
109+
const object = { a: 1 };
110+
Object.setPrototypeOf(object, { b: 2 });
111+
BSON.deserialize(BSON.serialize(object));
112+
// now returns { a: 1 } in v5.0
113+
// would have returned { a: 1, b: 2 } in v4.x
114+
```
115+
104116
### Negative Zero is now serialized to Double
105117

106118
BSON serialize will now preserve negative zero values as a floating point number.

src/extended_json.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
287287
if (typeof bsontype === 'undefined') {
288288
// It's a regular object. Recursively serialize its property values.
289289
const _doc: Document = {};
290-
for (const name in doc) {
290+
for (const name of Object.keys(doc)) {
291291
options.seenObjects.push({ propertyName: name, obj: null });
292292
try {
293293
const value = serializeValue(doc[name], options);

src/parser/calculate_size.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function calculateObjectSize(
2929
}
3030

3131
// Calculate size
32-
for (const key in object) {
32+
for (const key of Object.keys(object)) {
3333
totalLength += calculateElement(key, object[key], serializeFunctions, false, ignoreUndefined);
3434
}
3535
}

src/parser/deserializer.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -314,23 +314,16 @@ function deserializeObject(
314314
(buffer[index + 1] << 8) |
315315
(buffer[index + 2] << 16) |
316316
(buffer[index + 3] << 24);
317-
let arrayOptions = options;
317+
let arrayOptions: DeserializeOptions = options;
318318

319319
// Stop index
320320
const stopIndex = index + objectSize;
321321

322322
// All elements of array to be returned as raw bson
323323
if (fieldsAsRaw && fieldsAsRaw[name]) {
324-
arrayOptions = {};
325-
for (const n in options) {
326-
(
327-
arrayOptions as {
328-
[key: string]: DeserializeOptions[keyof DeserializeOptions];
329-
}
330-
)[n] = options[n as keyof DeserializeOptions];
331-
}
332-
arrayOptions['raw'] = true;
324+
arrayOptions = { ...options, raw: true };
333325
}
326+
334327
if (!globalUTFValidation) {
335328
arrayOptions = { ...arrayOptions, validation: { utf8: shouldValidateKey } };
336329
}

src/parser/serializer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ export function serializeInto(
817817
}
818818

819819
// Iterate over all the keys
820-
for (const key in object) {
820+
for (const key of Object.keys(object)) {
821821
let value = object[key];
822822
// Is there an override value
823823
if (typeof value?.toBSON === 'function') {

test/node/extended_json_tests.js renamed to test/node/extended_json.test.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
'use strict';
2-
3-
const BSON = require('../register-bson');
1+
import * as BSON from '../register-bson';
42
const EJSON = BSON.EJSON;
5-
const vm = require('vm');
3+
import * as vm from 'node:vm';
64

75
// BSON types
86
const Binary = BSON.Binary;
@@ -30,6 +28,7 @@ function getOldBSON() {
3028
try {
3129
// do a dynamic resolve to avoid exception when running browser tests
3230
const file = require.resolve('bson');
31+
// eslint-disable-next-line @typescript-eslint/no-var-requires
3332
const oldModule = require(file).BSON;
3433
const funcs = new oldModule.BSON();
3534
oldModule.serialize = funcs.serialize;
@@ -49,7 +48,7 @@ describe('Extended JSON', function () {
4948

5049
before(function () {
5150
const buffer = Buffer.alloc(64);
52-
for (var i = 0; i < buffer.length; i++) buffer[i] = i;
51+
for (let i = 0; i < buffer.length; i++) buffer[i] = i;
5352
const date = new Date();
5453
date.setTime(1488372056737);
5554
doc = {
@@ -80,15 +79,15 @@ describe('Extended JSON', function () {
8079

8180
it('should correctly extend an existing mongodb module', function () {
8281
// TODO(NODE-4377): doubleNumberIntFit should be a double not a $numberLong
83-
var json =
82+
const json =
8483
'{"_id":{"$numberInt":"100"},"gh":{"$numberInt":"1"},"binary":{"$binary":{"base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","subType":"00"}},"date":{"$date":{"$numberLong":"1488372056737"}},"code":{"$code":"function() {}","$scope":{"a":{"$numberInt":"1"}}},"dbRef":{"$ref":"tests","$id":{"$numberInt":"1"},"$db":"test"},"decimal":{"$numberDecimal":"100"},"double":{"$numberDouble":"10.1"},"int32":{"$numberInt":"10"},"long":{"$numberLong":"200"},"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"objectId":{"$oid":"111111111111111111111111"},"objectID":{"$oid":"111111111111111111111111"},"oldObjectID":{"$oid":"111111111111111111111111"},"regexp":{"$regularExpression":{"pattern":"hello world","options":"i"}},"symbol":{"$symbol":"symbol"},"timestamp":{"$timestamp":{"t":0,"i":1000}},"int32Number":{"$numberInt":"300"},"doubleNumber":{"$numberDouble":"200.2"},"longNumberIntFit":{"$numberLong":"7036874417766400"},"doubleNumberIntFit":{"$numberLong":"19007199250000000"}}';
8584

8685
expect(json).to.equal(EJSON.stringify(doc, null, 0, { relaxed: false }));
8786
});
8887

8988
it('should correctly deserialize using the default relaxed mode', function () {
9089
// Deserialize the document using non strict mode
91-
var doc1 = EJSON.parse(EJSON.stringify(doc, null, 0));
90+
let doc1 = EJSON.parse(EJSON.stringify(doc, null, 0));
9291

9392
// Validate the values
9493
expect(300).to.equal(doc1.int32Number);
@@ -109,23 +108,23 @@ describe('Extended JSON', function () {
109108

110109
it('should correctly serialize, and deserialize using built-in BSON', function () {
111110
// Create a doc
112-
var doc1 = {
111+
const doc1 = {
113112
int32: new Int32(10)
114113
};
115114

116115
// Serialize the document
117-
var text = EJSON.stringify(doc1, null, 0, { relaxed: false });
116+
const text = EJSON.stringify(doc1, null, 0, { relaxed: false });
118117
expect(text).to.equal('{"int32":{"$numberInt":"10"}}');
119118

120119
// Deserialize the json in strict and non strict mode
121-
var doc2 = EJSON.parse(text, { relaxed: false });
120+
let doc2 = EJSON.parse(text, { relaxed: false });
122121
expect(doc2.int32._bsontype).to.equal('Int32');
123122
doc2 = EJSON.parse(text);
124123
expect(doc2.int32).to.equal(10);
125124
});
126125

127126
it('should correctly serialize bson types when they are values', function () {
128-
var serialized = EJSON.stringify(new ObjectId('591801a468f9e7024b6235ea'), { relaxed: false });
127+
let serialized = EJSON.stringify(new ObjectId('591801a468f9e7024b6235ea'), { relaxed: false });
129128
expect(serialized).to.equal('{"$oid":"591801a468f9e7024b6235ea"}');
130129
serialized = EJSON.stringify(new ObjectID('591801a468f9e7024b6235ea'), { relaxed: false });
131130
expect(serialized).to.equal('{"$oid":"591801a468f9e7024b6235ea"}');
@@ -183,8 +182,8 @@ describe('Extended JSON', function () {
183182
expect(EJSON.parse('null')).to.be.null;
184183
expect(EJSON.parse('[null]')[0]).to.be.null;
185184

186-
var input = '{"result":[{"_id":{"$oid":"591801a468f9e7024b623939"},"emptyField":null}]}';
187-
var parsed = EJSON.parse(input);
185+
const input = '{"result":[{"_id":{"$oid":"591801a468f9e7024b623939"},"emptyField":null}]}';
186+
const parsed = EJSON.parse(input);
188187

189188
expect(parsed).to.deep.equal({
190189
result: [{ _id: new ObjectId('591801a468f9e7024b623939'), emptyField: null }]
@@ -334,14 +333,14 @@ describe('Extended JSON', function () {
334333
it('should work for function-valued and array-valued replacer parameters', function () {
335334
const doc = { a: new Int32(10), b: new Int32(10) };
336335

337-
var replacerArray = ['a', '$numberInt'];
338-
var serialized = EJSON.stringify(doc, replacerArray, 0, { relaxed: false });
336+
const replacerArray = ['a', '$numberInt'];
337+
let serialized = EJSON.stringify(doc, replacerArray, 0, { relaxed: false });
339338
expect(serialized).to.equal('{"a":{"$numberInt":"10"}}');
340339

341340
serialized = EJSON.stringify(doc, replacerArray);
342341
expect(serialized).to.equal('{"a":10}');
343342

344-
var replacerFunc = function (key, value) {
343+
const replacerFunc = function (key, value) {
345344
return key === 'b' ? undefined : value;
346345
};
347346
serialized = EJSON.stringify(doc, replacerFunc, 0, { relaxed: false });
@@ -352,11 +351,13 @@ describe('Extended JSON', function () {
352351
});
353352

354353
if (!usingOldBSON) {
355-
it.skip('skipping 4.x/1.x interop tests', () => {});
354+
it.skip('skipping 4.x/1.x interop tests', () => {
355+
// ignore
356+
});
356357
} else {
357358
it('should interoperate 4.x with 1.x versions of this library', function () {
358359
const buffer = Buffer.alloc(64);
359-
for (var i = 0; i < buffer.length; i++) {
360+
for (let i = 0; i < buffer.length; i++) {
360361
buffer[i] = i;
361362
}
362363
const [oldBsonObject, newBsonObject] = [OldBSON, BSON].map(bsonModule => {
@@ -454,7 +455,9 @@ describe('Extended JSON', function () {
454455
// by mongodb-core, then remove this test case and uncomment the MinKey checks in the test case above
455456
it('should interop with MinKey 1.x and 4.x, except the case that #310 breaks', function () {
456457
if (!usingOldBSON) {
457-
it.skip('interop tests', () => {});
458+
it.skip('interop tests', () => {
459+
// ignore
460+
});
458461
return;
459462
}
460463

@@ -516,7 +519,7 @@ describe('Extended JSON', function () {
516519
const serialized = EJSON.stringify(original);
517520
expect(serialized).to.equal('{"__proto__":{"a":42}}');
518521
const deserialized = EJSON.parse(serialized);
519-
expect(deserialized).to.have.deep.ownPropertyDescriptor('__proto__', {
522+
expect(deserialized).to.have.ownPropertyDescriptor('__proto__', {
520523
configurable: true,
521524
enumerable: true,
522525
writable: true,
@@ -527,7 +530,8 @@ describe('Extended JSON', function () {
527530

528531
context('circular references', () => {
529532
it('should throw a helpful error message for input with circular references', function () {
530-
const obj = {
533+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
534+
const obj: any = {
531535
some: {
532536
property: {
533537
array: []
@@ -542,7 +546,8 @@ Converting circular structure to EJSON:
542546
});
543547

544548
it('should throw a helpful error message for input with circular references, one-level nested', function () {
545-
const obj = {};
549+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
550+
const obj: any = {};
546551
obj.obj = obj;
547552
expect(() => EJSON.serialize(obj)).to.throw(`\
548553
Converting circular structure to EJSON:
@@ -551,7 +556,8 @@ Converting circular structure to EJSON:
551556
});
552557

553558
it('should throw a helpful error message for input with circular references, one-level nested inside base object', function () {
554-
const obj = {};
559+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
560+
const obj: any = {};
555561
obj.obj = obj;
556562
expect(() => EJSON.serialize({ foo: obj })).to.throw(`\
557563
Converting circular structure to EJSON:
@@ -560,7 +566,8 @@ Converting circular structure to EJSON:
560566
});
561567

562568
it('should throw a helpful error message for input with circular references, pointing back to base object', function () {
563-
const obj = { foo: {} };
569+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
570+
const obj: any = { foo: {} };
564571
obj.foo.obj = obj;
565572
expect(() => EJSON.serialize(obj)).to.throw(`\
566573
Converting circular structure to EJSON:
@@ -785,4 +792,13 @@ Converting circular structure to EJSON:
785792
expect(parsedUUID).to.deep.equal(expectedResult);
786793
});
787794
});
795+
796+
it('should only enumerate own property keys from input objects', () => {
797+
const input = { a: 1 };
798+
Object.setPrototypeOf(input, { b: 2 });
799+
const string = EJSON.stringify(input);
800+
expect(string).to.not.include(`"b":`);
801+
const result = JSON.parse(string);
802+
expect(result).to.deep.equal({ a: 1 });
803+
});
788804
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as BSON from '../../register-bson';
2+
import { expect } from 'chai';
3+
4+
describe('calculateSize()', () => {
5+
it('should only enumerate own property keys from input objects', () => {
6+
const input = { a: 1 };
7+
Object.setPrototypeOf(input, { b: 2 });
8+
expect(BSON.calculateObjectSize(input)).to.equal(12);
9+
});
10+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as BSON from '../../register-bson';
2+
import { expect } from 'chai';
3+
4+
describe('deserializer()', () => {
5+
describe('when the fieldsAsRaw options is present and has a value that corresponds to a key in the object', () => {
6+
it('ignores non-own properties set on the options object', () => {
7+
const bytes = BSON.serialize({ someKey: [1] });
8+
const options = { fieldsAsRaw: { someKey: true } };
9+
Object.setPrototypeOf(options, { promoteValues: false });
10+
const result = BSON.deserialize(bytes, options);
11+
expect(result).to.have.property('someKey').that.is.an('array');
12+
expect(
13+
result.someKey[0],
14+
'expected promoteValues option set on options object prototype to be ignored, but it was not'
15+
).to.not.have.property('_bsontype', 'Int32');
16+
});
17+
});
18+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as BSON from '../../register-bson';
2+
import { bufferFromHexArray } from '../tools/utils';
3+
import { expect } from 'chai';
4+
5+
describe('serialize()', () => {
6+
it('should only enumerate own property keys from input objects', () => {
7+
const input = { a: 1 };
8+
Object.setPrototypeOf(input, { b: 2 });
9+
const bytes = BSON.serialize(input);
10+
expect(bytes).to.deep.equal(
11+
bufferFromHexArray([
12+
'106100', // type int32, a\x00
13+
'01000000' // int32LE = 1
14+
])
15+
);
16+
});
17+
});

0 commit comments

Comments
 (0)