diff --git a/packages/merge-patch/README.md b/packages/merge-patch/README.md index 622a873..717bc49 100644 --- a/packages/merge-patch/README.md +++ b/packages/merge-patch/README.md @@ -69,6 +69,18 @@ person = mergePatch.apply(person, patch) [↑](#json8-merge-patch) +### object creation + +When needed, `apply` creates objects with `null` prototype, you can choose the prototype to use with `{proto: Object}` as a third argument. + +[↑](#json8-merge-patch) + +### prototype pollution + +`apply` will throw with an error if [prototype pollution](https://github.com/HoLyVieR/prototype-pollution-nsec18) is attempted. You can allow for prototype pollution by passing `{pollute: true}` as a third argument. + +[↑](#json8-merge-patch) + ### patch Alias for [apply](#apply) method. diff --git a/packages/merge-patch/lib/apply.js b/packages/merge-patch/lib/apply.js index 5415d2b..9fc9453 100644 --- a/packages/merge-patch/lib/apply.js +++ b/packages/merge-patch/lib/apply.js @@ -5,21 +5,29 @@ const OBJECT = "object"; /** * Apply a JSON merge patch onto a document * https://tools.ietf.org/html/rfc7396 - * @param {Object} doc - JSON object document - * @param {Object} patch - JSON object patch - * @return {Object} - JSON object document + * @param {Object} doc - JSON object document + * @param {Object} patch - JSON object patch + * @param {Object} [options] - options + * @param {Boolean} [options.pollute=false] - Allow prototype pollution - throw otherwise + * @param {Object} [options.proto=null] - Prototype to use for object creation + * @return {Object} - JSON object document */ -module.exports = function apply(doc, patch) { +module.exports = function apply(doc, patch, options) { if (typeof patch !== OBJECT || patch === null || Array.isArray(patch)) { return patch; } + options = options || Object.create(null); + if (typeof doc !== OBJECT || doc === null || Array.isArray(doc)) { - doc = Object.create(null); + doc = Object.create(options.proto || null); } const keys = Object.keys(patch); for (const key of keys) { + if (options.pollute !== true && key === "__proto__") { + throw new Error("Prototype pollution attempt"); + } const v = patch[key]; if (v === null) { delete doc[key]; diff --git a/packages/merge-patch/test/apply.js b/packages/merge-patch/test/apply.js index 4a8b799..b9de955 100644 --- a/packages/merge-patch/test/apply.js +++ b/packages/merge-patch/test/apply.js @@ -51,23 +51,21 @@ describe("apply", () => { assert.deepEqual(doc, {}); }); - // https://github.com/lodash/lodash/pull/4337 + // https://github.com/sonnyp/JSON8/issues/113 + // https://github.com/HoLyVieR/prototype-pollution-nsec18 it("prevents prototype pollution", () => { let doc = {}; - const patch = { __proto__: { foobar: true } }; - doc = apply(doc, patch); + const patch = JSON.parse('{ "__proto__": { "isAdmin": true }}'); - assert.deepEqual(doc, {}); - }); + assert.throws( + () => { + doc = apply(doc, patch); + }, + Error, + "Prototype pollution attempt" + ); - // https://github.com/lodash/lodash/pull/4336 - it("prevents constructor pollution", () => { - let doc = {}; - - const patch = { constructor: { foo: "bar" } }; - doc = apply(doc, patch); - assert.equal("foo" in Object, false); - assert.equal(Object.foo, undefined); - assert.deepEqual(doc, patch); + assert.equal(doc.isAdmin, undefined); + assert.equal("isAdmin" in doc, false); }); });