From bab8b58d4a61ee40ab27bcc099b09cf6629b9b33 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 1 Apr 2026 15:37:53 -0400 Subject: [PATCH] fix(clone): support cloning objects that have an own __proto__ property Fix #16202 --- lib/helpers/clone.js | 10 +++-- test/helpers/clone.test.js | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 11d46185e7d..dbd2fa5f345 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -2,7 +2,6 @@ const Decimal = require('../types/decimal128'); const ObjectId = require('../types/objectid'); -const specialProperties = require('./specialProperties'); const isMongooseObject = require('./isMongooseObject'); const getFunctionName = require('./getFunctionName'); const isBsonType = require('./isBsonType'); @@ -16,6 +15,8 @@ const UUID = BSON.UUID; const Binary = BSON.Binary; +const objWith__proto__ = { ['__proto__']: null }; + /** * Object clone with Mongoose natives support. * @@ -161,7 +162,7 @@ function cloneObject(obj, options, isArrayChild) { const minimize = options?.minimize; const omitUndefined = options?.omitUndefined; const seen = options?._seen; - const ret = {}; + let ret = {}; let hasKeys; if (seen && seen.has(obj)) { @@ -175,10 +176,13 @@ function cloneObject(obj, options, isArrayChild) { const keys = Object.keys(obj); const len = keys.length; + if (keys.includes('__proto__')) { + ret = { ...objWith__proto__ }; + } for (let i = 0; i < len; ++i) { const key = keys[i]; - if (specialProperties.has(key)) { + if (key === 'constructor' || key === 'prototype') { continue; } diff --git a/test/helpers/clone.test.js b/test/helpers/clone.test.js index 80540041c2b..e56679ff45d 100644 --- a/test/helpers/clone.test.js +++ b/test/helpers/clone.test.js @@ -309,4 +309,80 @@ describe('clone', () => { const out = clone(o, { bson: true }); assert.deepEqual(out, o); }); + + describe('__proto__ security', () => { + it('cloning an object with own __proto__ does not pollute Object.prototype', () => { + const malicious = JSON.parse('{"__proto__":{"polluted":"yes"}}'); + clone(malicious); + + assert.strictEqual(Object.prototype.polluted, undefined); + assert.strictEqual({}.polluted, undefined); + + clone({ ['__proto__']: 'polluted' }); + assert.strictEqual(Object.prototype.polluted, undefined); + assert.strictEqual({}.polluted, undefined); + }); + + it('cloning nested __proto__ does not pollute Object.prototype', () => { + const malicious = JSON.parse('{"nested":{"__proto__":{"polluted":"nested"}}}'); + clone(malicious); + + assert.strictEqual(Object.prototype.polluted, undefined); + assert.strictEqual({}.polluted, undefined); + }); + + it('__proto__ with constructor.prototype path does not pollute', () => { + const malicious = JSON.parse('{"__proto__":{"toString":"hacked"}}'); + clone(malicious); + + assert.strictEqual(typeof {}.toString, 'function'); + assert.strictEqual(Object.prototype.toString.call({}), '[object Object]'); + }); + + it('cloned __proto__ is an own property, not a prototype link', () => { + const obj = JSON.parse('{"__proto__":{"x":1},"a":2}'); + const out = clone(obj); + + assert.strictEqual(out.a, 2); + // __proto__ should exist as an own property on the cloned object + assert.ok(Object.prototype.hasOwnProperty.call(out, '__proto__')); + // x from __proto__ value must NOT leak as an inherited property + assert.strictEqual(out.x, undefined); + // Object.prototype must remain untouched + assert.strictEqual(Object.prototype.x, undefined); + }); + + it('deeply nested __proto__ does not pollute', () => { + const malicious = JSON.parse('{"a":{"b":{"__proto__":{"deep":"pollution"}}}}'); + clone(malicious); + + assert.strictEqual(Object.prototype.deep, undefined); + assert.strictEqual({}.deep, undefined); + }); + + it('__proto__ set to a primitive value does not break clone', () => { + const obj = JSON.parse('{"__proto__":"string_value","a":1}'); + const out = clone(obj); + + assert.strictEqual(out.a, 1); + assert.ok(Object.prototype.hasOwnProperty.call(out, '__proto__')); + }); + + it('__proto__ set to null does not break clone', () => { + const obj = JSON.parse('{"__proto__":null,"a":1}'); + const out = clone(obj); + + assert.strictEqual(out.a, 1); + assert.strictEqual(out.__proto__, null); + }); + + it('multiple __proto__ in array elements do not pollute', () => { + const arr = JSON.parse('[{"__proto__":{"arrPolluted":"yes"}},{"__proto__":{"arrPolluted2":"yes"}}]'); + clone(arr); + + assert.strictEqual(Object.prototype.arrPolluted, undefined); + assert.strictEqual(Object.prototype.arrPolluted2, undefined); + assert.strictEqual(arr[0].__proto__.arrPolluted, 'yes'); + }); + }); });