Skip to content

Commit fd1a9e2

Browse files
committed
fix: prevent prototype pollution (#41)
1 parent bfd52c3 commit fd1a9e2

File tree

2 files changed

+37
-4
lines changed

2 files changed

+37
-4
lines changed

src/merge.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function mergeRecursively<T1, T2>(
5050
const symbols = Object.getOwnPropertySymbols(origin)
5151
newObject = [...props, ...symbols].reduce(
5252
(carry, key) => {
53+
// Skip __proto__ properties to prevent prototype poisoning
54+
if (key === '__proto__') return carry
5355
const targetVal = origin[key as string]
5456
if (
5557
(!isSymbol(key) && !Object.getOwnPropertyNames(newComer).includes(key)) ||
@@ -71,6 +73,8 @@ function mergeRecursively<T1, T2>(
7173
const props = Object.getOwnPropertyNames(newComer)
7274
const symbols = Object.getOwnPropertySymbols(newComer)
7375
const result = [...props, ...symbols].reduce((carry, key) => {
76+
// Skip __proto__ properties to prevent prototype poisoning
77+
if (key === '__proto__') return carry
7478
// re-define the origin and newComer as targetVal and newVal
7579
let newVal = newComer[key as string]
7680
const targetVal = isPlainObject(origin) ? origin[key as string] : undefined
@@ -91,9 +95,8 @@ function mergeRecursively<T1, T2>(
9195
}
9296

9397
/**
94-
* Merge anything recursively.
95-
* Objects get merged, special objects (classes etc.) are re-assigned "as is".
96-
* Basic types overwrite objects or other basic types.
98+
* Merge anything recursively. Objects get merged, special objects (classes etc.) are re-assigned
99+
* "as is". Basic types overwrite objects or other basic types.
97100
*/
98101
export function merge<T, const Tn extends unknown[]>(object: T, ...otherObjects: Tn): Merge<T, Tn> {
99102
return otherObjects.reduce((result, newComer) => {

test/index.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { test, expect } from 'vitest'
21
import { isDate } from 'is-what'
2+
import { expect, test } from 'vitest'
33
import { merge } from '../src/index'
44

55
function copy<T>(any: T): T {
@@ -297,3 +297,33 @@ test('readme', () => {
297297
is: 'cool',
298298
})
299299
})
300+
301+
test('prototype pollution', () => {
302+
// Test object with default permissions
303+
const defaultPermissions = {
304+
read: true,
305+
write: false,
306+
delete: false,
307+
}
308+
309+
// Attempt prototype pollution through __proto__
310+
const maliciousPayload = JSON.parse('{"__proto__": { "isAdmin": true }}')
311+
const mergedPermissions = merge({}, defaultPermissions, maliciousPayload)
312+
313+
// Verify that prototype pollution was prevented
314+
expect(mergedPermissions.isAdmin).toBeUndefined()
315+
expect(Object.getPrototypeOf(mergedPermissions)).toBe(Object.prototype)
316+
317+
// Test with constructor and prototype properties
318+
const maliciousPayload2 = {
319+
constructor: { prototype: { isAdmin: true } },
320+
}
321+
const mergedPermissions2 = merge({}, defaultPermissions, maliciousPayload2)
322+
expect((mergedPermissions2 as any).isAdmin).toBeUndefined()
323+
expect(Object.getPrototypeOf(mergedPermissions2)).toBe(Object.prototype)
324+
325+
// Verify original properties are still intact
326+
expect(mergedPermissions.read).toBe(true)
327+
expect(mergedPermissions.write).toBe(false)
328+
expect(mergedPermissions.delete).toBe(false)
329+
})

0 commit comments

Comments
 (0)