-
-
Notifications
You must be signed in to change notification settings - Fork 4k
fix(clone): support cloning objects that have an own __proto__ property #16205
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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__ }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe some documentation would be good here as to why this specific way is required.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seconded, Potential perf improvement instead of using |
||
| } | ||
|
Comment on lines
165
to
+181
|
||
|
|
||
| for (let i = 0; i < len; ++i) { | ||
| const key = keys[i]; | ||
| if (specialProperties.has(key)) { | ||
| if (key === 'constructor' || key === 'prototype') { | ||
| continue; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+375
to
+376
|
||||||||||||||||||||||||||||||||||||||||||||||
| assert.strictEqual(out.a, 1); | |
| assert.strictEqual(out.__proto__, null); | |
| assert.strictEqual(out.a, 1); | |
| // __proto__ should be an own data property with value null, not a prototype link | |
| assert.strictEqual(out.__proto__, null); | |
| assert.ok(Object.prototype.hasOwnProperty.call(out, '__proto__')); | |
| assert.strictEqual(Object.getPrototypeOf(out), Object.prototype); |
Copilot
AI
Apr 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only checks that the input arr still has its __proto__ value and that Object.prototype wasn't polluted, but it doesn't assert anything about the clone result. Consider asserting the returned cloned array's elements have an own __proto__ property (and that Object.getPrototypeOf(cloned[i]) === Object.prototype) to validate the new behavior for arrays, not just the absence of global pollution.
| clone(arr); | |
| assert.strictEqual(Object.prototype.arrPolluted, undefined); | |
| assert.strictEqual(Object.prototype.arrPolluted2, undefined); | |
| assert.strictEqual(arr[0].__proto__.arrPolluted, 'yes'); | |
| const cloned = clone(arr); | |
| // Object.prototype must not be polluted | |
| assert.strictEqual(Object.prototype.arrPolluted, undefined); | |
| assert.strictEqual(Object.prototype.arrPolluted2, undefined); | |
| // Original array elements keep their own __proto__ values | |
| assert.strictEqual(arr[0].__proto__.arrPolluted, 'yes'); | |
| assert.strictEqual(arr[1].__proto__.arrPolluted2, 'yes'); | |
| // Cloned array elements should have __proto__ as an own property, not via prototype chain | |
| assert.ok(Object.prototype.hasOwnProperty.call(cloned[0], '__proto__')); | |
| assert.ok(Object.prototype.hasOwnProperty.call(cloned[1], '__proto__')); | |
| assert.strictEqual(Object.getPrototypeOf(cloned[0]), Object.prototype); | |
| assert.strictEqual(Object.getPrototypeOf(cloned[1]), Object.prototype); | |
| assert.strictEqual(cloned[0].__proto__.arrPolluted, 'yes'); | |
| assert.strictEqual(cloned[1].__proto__.arrPolluted2, 'yes'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW
specialProperties(which includes__proto__) is used in 7 files, not justclone(). I quickly scanned through them and I think some of them have the same problem of data loss as well and should be updated.