Skip to content

Commit f7a6a05

Browse files
authored
fix(shared): prevent prototype pollution in deep merge utilities (#7621)
1 parent 79e2622 commit f7a6a05

File tree

3 files changed

+121
-1
lines changed

3 files changed

+121
-1
lines changed

.changeset/secure-foxes-guard.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/shared": patch
3+
---
4+
5+
Fix prototype pollution vulnerability in `fastDeepMergeAndReplace` and `fastDeepMergeAndKeep` utilities by blocking dangerous keys (`__proto__`, `constructor`, `prototype`) during object merging.

packages/shared/src/__tests__/fastDeepMerge.spec.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, describe, expect, it } from 'vitest';
22

33
import { fastDeepMergeAndKeep, fastDeepMergeAndReplace } from '../utils/fastDeepMerge';
44

5+
// Helper to clean up any accidental prototype pollution during tests
6+
afterEach(() => {
7+
// @ts-expect-error - cleaning up potential pollution
8+
delete Object.prototype.polluted;
9+
// @ts-expect-error - cleaning up potential pollution
10+
delete Object.prototype.isAdmin;
11+
});
12+
513
describe('fastDeepMergeReplace', () => {
614
it('merges simple objects', () => {
715
const source = { a: '1', b: '2', c: '3' };
@@ -61,3 +69,99 @@ describe('fastDeepMergeKeep', () => {
6169
expect(target).toEqual({ a: '10', b: '2', c: '3', obj: { a: '10', b: '20' } });
6270
});
6371
});
72+
73+
describe('prototype pollution prevention', () => {
74+
describe('fastDeepMergeAndReplace', () => {
75+
it('should not pollute Object.prototype via __proto__', () => {
76+
const payload = JSON.parse('{"__proto__": {"polluted": "true"}}');
77+
const target = {};
78+
79+
fastDeepMergeAndReplace(payload, target);
80+
81+
// @ts-expect-error - checking for pollution
82+
expect(Object.prototype.polluted).toBeUndefined();
83+
// @ts-expect-error - checking for pollution
84+
expect({}.polluted).toBeUndefined();
85+
});
86+
87+
it('should not pollute via constructor.prototype', () => {
88+
const payload = { constructor: { prototype: { isAdmin: true } } };
89+
const target = {};
90+
91+
fastDeepMergeAndReplace(payload, target);
92+
93+
// @ts-expect-error - checking for pollution
94+
expect(Object.prototype.isAdmin).toBeUndefined();
95+
// @ts-expect-error - checking for pollution
96+
expect({}.isAdmin).toBeUndefined();
97+
});
98+
99+
it('should not pollute via nested __proto__', () => {
100+
const payload = JSON.parse('{"nested": {"__proto__": {"polluted": "nested"}}}');
101+
const target = { nested: {} };
102+
103+
fastDeepMergeAndReplace(payload, target);
104+
105+
// @ts-expect-error - checking for pollution
106+
expect(Object.prototype.polluted).toBeUndefined();
107+
});
108+
109+
it('should still merge safe keys normally', () => {
110+
const payload = JSON.parse('{"__proto__": {"polluted": "true"}, "safe": "value"}');
111+
const target = {};
112+
113+
fastDeepMergeAndReplace(payload, target);
114+
115+
expect(target).toEqual({ safe: 'value' });
116+
// @ts-expect-error - checking for pollution
117+
expect(Object.prototype.polluted).toBeUndefined();
118+
});
119+
});
120+
121+
describe('fastDeepMergeAndKeep', () => {
122+
it('should not pollute Object.prototype via __proto__', () => {
123+
const payload = JSON.parse('{"__proto__": {"polluted": "true"}}');
124+
const target = {};
125+
126+
fastDeepMergeAndKeep(payload, target);
127+
128+
// @ts-expect-error - checking for pollution
129+
expect(Object.prototype.polluted).toBeUndefined();
130+
// @ts-expect-error - checking for pollution
131+
expect({}.polluted).toBeUndefined();
132+
});
133+
134+
it('should not pollute via constructor.prototype', () => {
135+
const payload = { constructor: { prototype: { isAdmin: true } } };
136+
const target = {};
137+
138+
fastDeepMergeAndKeep(payload, target);
139+
140+
// @ts-expect-error - checking for pollution
141+
expect(Object.prototype.isAdmin).toBeUndefined();
142+
// @ts-expect-error - checking for pollution
143+
expect({}.isAdmin).toBeUndefined();
144+
});
145+
146+
it('should not pollute via nested __proto__', () => {
147+
const payload = JSON.parse('{"nested": {"__proto__": {"polluted": "nested"}}}');
148+
const target = { nested: {} };
149+
150+
fastDeepMergeAndKeep(payload, target);
151+
152+
// @ts-expect-error - checking for pollution
153+
expect(Object.prototype.polluted).toBeUndefined();
154+
});
155+
156+
it('should still merge safe keys normally', () => {
157+
const payload = JSON.parse('{"__proto__": {"polluted": "true"}, "safe": "value"}');
158+
const target = {};
159+
160+
fastDeepMergeAndKeep(payload, target);
161+
162+
expect(target).toEqual({ safe: 'value' });
163+
// @ts-expect-error - checking for pollution
164+
expect(Object.prototype.polluted).toBeUndefined();
165+
});
166+
});
167+
});

packages/shared/src/utils/fastDeepMerge.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Keys that could lead to prototype pollution attacks
2+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
3+
14
/**
25
* Merges 2 objects without creating new object references
36
* The merged props will appear on the `target` object
@@ -12,6 +15,10 @@ export const fastDeepMergeAndReplace = (
1215
}
1316

1417
for (const key in source) {
18+
// Skip dangerous keys to prevent prototype pollution
19+
if (DANGEROUS_KEYS.has(key)) {
20+
continue;
21+
}
1522
if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== null && typeof source[key] === `object`) {
1623
if (target[key] === undefined) {
1724
target[key] = new (Object.getPrototypeOf(source[key]).constructor)();
@@ -32,6 +39,10 @@ export const fastDeepMergeAndKeep = (
3239
}
3340

3441
for (const key in source) {
42+
// Skip dangerous keys to prevent prototype pollution
43+
if (DANGEROUS_KEYS.has(key)) {
44+
continue;
45+
}
3546
if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== null && typeof source[key] === `object`) {
3647
if (target[key] === undefined) {
3748
target[key] = new (Object.getPrototypeOf(source[key]).constructor)();

0 commit comments

Comments
 (0)