Skip to content

Commit

Permalink
Copy own __proto__ safely (#164)
Browse files Browse the repository at this point in the history
Copy own __proto__ safely
  • Loading branch information
TehShrike committed Oct 21, 2019
2 parents 7fd58e8 + c576569 commit 2b093b7
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 0 deletions.
17 changes: 17 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ function getKeys(target) {
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
}

// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe(target, key) {
try {
return (key in target) // Properties are safe to merge if they don't exist in the target yet,
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
} catch (unused) {
// Counterintuitively, it's safe to merge any property on a target that causes the `in` operator to throw.
// This happens when trying to copy an object in the source over a plain string in the target.
return false
}
}

function mergeObject(target, source, options) {
var destination = {}
if (options.isMergeableObject(target)) {
Expand All @@ -44,6 +57,10 @@ function mergeObject(target, source, options) {
})
}
getKeys(source).forEach(function(key) {
if (propertyIsUnsafe(target, key)) {
return
}

if (!options.isMergeableObject(source[key]) || !target[key]) {
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options)
} else {
Expand Down
78 changes: 78 additions & 0 deletions test/prototype-poisoning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
var merge = require('../')
var test = require('tape')
var isMergeableObject = require('is-mergeable-object')

test('merging objects with own __proto__', function(t) {
var user = {}
var malicious = JSON.parse('{ "__proto__": { "admin": true } }')
var mergedObject = merge(user, malicious)
t.notOk(mergedObject.__proto__.admin, 'non-plain properties should not be merged')
t.notOk(mergedObject.admin, 'the destination should have an unmodified prototype')
t.end()
})

test('merging objects with plain and non-plain properties', function(t) {
var plainSymbolKey = Symbol('plainSymbolKey')
var parent = {
parentKey: 'should be undefined'
}

var target = Object.create(parent)
target.plainKey = 'should be replaced'
target[plainSymbolKey] = 'should also be replaced'

var source = {
parentKey: 'foo',
plainKey: 'bar',
newKey: 'baz',
[plainSymbolKey]: 'qux'
}

var mergedObject = merge(target, source)
t.equal(undefined, mergedObject.parentKey, 'inherited properties of target should be removed, not merged or ignored')
t.equal('bar', mergedObject.plainKey, 'enumerable own properties of target should be merged')
t.equal('baz', mergedObject.newKey, 'properties not yet on target should be merged')
t.equal('qux', mergedObject[plainSymbolKey], 'enumerable own symbol properties of target should be merged')
t.end()
})

// the following cases come from the thread here: https://github.com/TehShrike/deepmerge/pull/164
test('merging strings works with a custom string merge', function(t) {
var target = { name: "Alexander" }
var source = { name: "Hamilton" }
function customMerge(key, options) {
if (key === 'name') {
return function(target, source, options) {
return target[0] + '. ' + source.substring(0, 3)
}
} else {
return merge
}
}

function mergeable(target) {
return isMergeableObject(target) || (typeof target === 'string' && target.length > 1)
}

t.equal('A. Ham', merge(target, source, { customMerge: customMerge, isMergeableObject: mergeable }).name)
t.end()
})

test('merging objects with null prototype', function(t) {
var target = Object.create(null)
var source = Object.create(null)
target.wheels = 4
target.trunk = { toolbox: ['hammer'] }
source.trunk = { toolbox: ['wrench'] }
source.engine = 'v8'
var expected = {
wheels: 4,
engine: 'v8',
trunk: {
toolbox: ['hammer', 'wrench' ]
}
}

t.deepEqual(expected, merge(target, source))
t.end()
})

0 comments on commit 2b093b7

Please sign in to comment.