Skip to content

Commit 4944cf4

Browse files
hi-ogawacodex
andauthored
fix(utils): fix deepMerge to handle prototype (#11215)
Co-authored-by: Hiroshi Ogawa <[email protected]> Co-authored-by: Codex (GPT-6) <[email protected]>
1 parent c119be0 commit 4944cf4

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

packages/utils/src/helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@ export function deepMerge<T extends object = object>(
407407

408408
if (isMergeableObject(target) && isMergeableObject(source)) {
409409
(Object.keys(source) as (keyof T)[]).forEach((key) => {
410+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
411+
return
412+
}
410413
const _source = source as T
411414
if (isMergeableObject(_source[key])) {
412415
if (!target[key]) {

test/unit/test/utils.spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { assertTypes, deepClone, deepMerge, isNegativeNaN, objectAttr, toArray } from '@vitest/utils/helpers'
22
import { parseSingleFFOrSafariStack } from '@vitest/utils/source-map'
33
import { EvaluatedModules } from 'vite/module-runner'
4-
import { beforeAll, describe, expect, test } from 'vitest'
4+
import { beforeAll, describe, expect, onTestFinished, test } from 'vitest'
55
import { deepMergeSnapshot } from '../../../packages/snapshot/src/port/utils'
66
import { resetModules } from '../../../packages/vitest/src/runtime/utils'
77

@@ -24,6 +24,62 @@ describe('assertTypes', () => {
2424
})
2525

2626
describe('deepMerge', () => {
27+
test('does not merge prototype mutation properties', () => {
28+
onTestFinished(() => {
29+
Reflect.deleteProperty(Object.prototype, 'rootPolluted')
30+
Reflect.deleteProperty(Object.prototype, 'nestedPolluted')
31+
})
32+
33+
const source = JSON.parse(`{
34+
"__proto__": { "rootPolluted": true },
35+
"constructor": { "name": "Object", "injected": true },
36+
"prototype": { "injected": true },
37+
"nested": {
38+
"__proto__": { "nestedPolluted": true },
39+
"constructor": { "name": "Object", "injected": true },
40+
"prototype": { "injected": true }
41+
}
42+
}`)
43+
44+
const merged = deepMerge({
45+
constructor: { name: 'Object', preserved: true },
46+
prototype: { preserved: true },
47+
nested: {
48+
constructor: { name: 'Object', preserved: true },
49+
prototype: { preserved: true },
50+
},
51+
}, source)
52+
53+
expect({
54+
merged,
55+
rootPolluted: ({} as any).rootPolluted,
56+
nestedPolluted: ({} as any).nestedPolluted,
57+
}).toMatchInlineSnapshot(`
58+
{
59+
"merged": {
60+
"constructor": {
61+
"name": "Object",
62+
"preserved": true,
63+
},
64+
"nested": {
65+
"constructor": {
66+
"name": "Object",
67+
"preserved": true,
68+
},
69+
"prototype": {
70+
"preserved": true,
71+
},
72+
},
73+
"prototype": {
74+
"preserved": true,
75+
},
76+
},
77+
"nestedPolluted": undefined,
78+
"rootPolluted": undefined,
79+
}
80+
`)
81+
})
82+
2783
test('non plain objects retain their prototype, arrays are not merging, plain objects are merging', () => {
2884
class TestA {
2985
baz = 'baz'

0 commit comments

Comments
 (0)