Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const Decimal = require('../types/decimal128');
const ObjectId = require('../types/objectid');
const specialProperties = require('./specialProperties');
Copy link
Copy Markdown
Collaborator

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 just clone(). I quickly scanned through them and I think some of them have the same problem of data loss as well and should be updated.

const isMongooseObject = require('./isMongooseObject');
const getFunctionName = require('./getFunctionName');
const isBsonType = require('./isBsonType');
Expand All @@ -16,6 +15,8 @@ const UUID = BSON.UUID;

const Binary = BSON.Binary;

const objWith__proto__ = { ['__proto__']: null };

/**
* Object clone with Mongoose natives support.
*
Expand Down Expand Up @@ -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)) {
Expand All @@ -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__ };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

@AbdelrahmanHafez AbdelrahmanHafez Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seconded, also not quite sure whether the includes check is broader than it should be.
Edit: That's the right check, keys is an array, not a string.

Potential perf improvement instead of using keys.includes('__proto__') is to use Object.hasOwn('__proto__')

}
Comment on lines 165 to +181
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ret is written to options._seen (and may have trustedSymbol copied onto it) before potentially being reassigned when __proto__ is present. Reassigning ret breaks cycle handling (seen still points at the original {}) and drops any previously-copied trustedSymbol. Initialize ret based on whether obj has an own enumerable __proto__ before calling seen.set() / copying trustedSymbol (or update seen and reapply trustedSymbol after changing ret).

Copilot uses AI. Check for mistakes.

for (let i = 0; i < len; ++i) {
const key = keys[i];
if (specialProperties.has(key)) {
if (key === 'constructor' || key === 'prototype') {
continue;
}

Expand Down
76 changes: 76 additions & 0 deletions test/helpers/clone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts out.__proto__ === null, but that can also be true if the clone accidentally sets the object's prototype to null (via the legacy __proto__ setter) rather than creating an own __proto__ data property. To ensure the intended behavior, also assert hasOwnProperty('__proto__') and that Object.getPrototypeOf(out) is still Object.prototype.

Suggested change
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 uses AI. Check for mistakes.
});

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');
Comment on lines +381 to +385
Copy link

Copilot AI Apr 1, 2026

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.

Suggested change
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');

Copilot uses AI. Check for mistakes.
});
});
});
Loading