Skip to content

Commit 57b02e9

Browse files
committed
feat!(NODE-4410): only enumerate own properties
1 parent d705d75 commit 57b02e9

File tree

9 files changed

+85
-37
lines changed

9 files changed

+85
-37
lines changed

docs/upgrade-to-v5.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ We have set our typescript compilation target to `es2020` which aligns with our
7979
> **TL;DR**: TODO
8080
8181
TODO(NODE-4771): serializeFunctions bug fix makes function names outside the ascii range get serialized correctly
82+
83+
### BSON Element names are now fetched only from object's own properties
84+
85+
Previously objects passed to the `BSON.serialize`, `BSON.calculateObjectSize`, and `EJSON.stringify` API would have the element names enumerated with a `for-in` loop which will emit keys defined on the prototype. Since this is likely surprising, especially if a globally shared prototype has been modified we are now using `Object.keys` to enumerate the element names from a js object.

src/extended_json.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) {
282282
if (typeof bsontype === 'undefined') {
283283
// It's a regular object. Recursively serialize its property values.
284284
const _doc: Document = {};
285-
for (const name in doc) {
285+
for (const name of Object.keys(doc)) {
286286
options.seenObjects.push({ propertyName: name, obj: null });
287287
try {
288288
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
@@ -827,7 +827,7 @@ export function serializeInto(
827827
}
828828

829829
// Iterate over all the keys
830-
for (const key in object) {
830+
for (const key of Object.keys(object)) {
831831
let value = object[key];
832832
// Is there an override value
833833
if (typeof value?.toBSON === 'function') {

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

Lines changed: 39 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
// Serialize the document
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);
@@ -108,23 +107,23 @@ describe('Extended JSON', function () {
108107

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

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

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

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

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

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

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

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

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

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

@@ -515,7 +518,7 @@ describe('Extended JSON', function () {
515518
const serialized = EJSON.stringify(original);
516519
expect(serialized).to.equal('{"__proto__":{"a":42}}');
517520
const deserialized = EJSON.parse(serialized);
518-
expect(deserialized).to.have.deep.ownPropertyDescriptor('__proto__', {
521+
expect(deserialized).to.have.ownPropertyDescriptor('__proto__', {
519522
configurable: true,
520523
enumerable: true,
521524
writable: true,
@@ -526,7 +529,8 @@ describe('Extended JSON', function () {
526529

527530
context('circular references', () => {
528531
it('should throw a helpful error message for input with circular references', function () {
529-
const obj = {
532+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
533+
const obj: any = {
530534
some: {
531535
property: {
532536
array: []
@@ -541,7 +545,8 @@ Converting circular structure to EJSON:
541545
});
542546

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

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

561567
it('should throw a helpful error message for input with circular references, pointing back to base object', function () {
562-
const obj = { foo: {} };
568+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
569+
const obj: any = { foo: {} };
563570
obj.foo.obj = obj;
564571
expect(() => EJSON.serialize(obj)).to.throw(`\
565572
Converting circular structure to EJSON:
@@ -784,4 +791,12 @@ Converting circular structure to EJSON:
784791
expect(parsedUUID).to.deep.equal(expectedResult);
785792
});
786793
});
794+
795+
it('should only enumerate own property keys from input objects', () => {
796+
const input = { a: 1 };
797+
Object.setPrototypeOf(input, { b: 2 });
798+
const string = EJSON.stringify(input);
799+
expect(string).to.include(`"a":`);
800+
expect(string).to.not.include(`"b":`);
801+
});
787802
});
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as BSON from '../../register-bson';
2+
import { expect } from 'chai';
3+
4+
describe('deserializer()', () => {
5+
it('should only enumerate own property keys from input options', () => {
6+
const bytes = BSON.serialize({ someKey: [1] });
7+
const options = { fieldsAsRaw: { someKey: true } };
8+
Object.setPrototypeOf(options, { promoteValues: false });
9+
const result = BSON.deserialize(bytes, options);
10+
expect(result).to.have.property('someKey').that.is.an('array');
11+
expect(result.someKey[0]).to.not.have.property('_bsontype', 'Int32');
12+
});
13+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as BSON from '../../register-bson';
2+
import { expect } from 'chai';
3+
4+
describe('serialize()', () => {
5+
it('should only enumerate own property keys from input objects', () => {
6+
const input = { a: 1 };
7+
Object.setPrototypeOf(input, { b: 2 });
8+
const bytes = BSON.serialize(input);
9+
expect(bytes).to.have.property('byteLength', 12);
10+
expect(Array.from(bytes)).to.include('a'.charCodeAt(0));
11+
expect(Array.from(bytes)).to.not.include('b'.charCodeAt(0));
12+
});
13+
});

0 commit comments

Comments
 (0)