diff --git a/README.md b/README.md index 02ce36d7..c2c869d6 100644 --- a/README.md +++ b/README.md @@ -182,14 +182,20 @@ else { ## API -#### `jsonpatch.applyPatch(document: any, patch: Operation[], validateOperation: Boolean | Function = false): OperationResult[]` +#### `function applyPatch(document: T, patch: Operation[], validateOperation?: boolean | Validator, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): PatchResult` Applies `patch` array on `obj`. +- `document` The document to patch +- `patch` a JSON-Patch array of operations to apply +- `validateOperation` Boolean for whether to validate each operation with our default validator, or to pass a validator callback +- `mutateDocument` Whether to mutate the original document or clone it before applying +- `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. + An invalid patch results in throwing an error (see `jsonpatch.validate` for more information about the error object). It modifies the `document` object and `patch` - it gets the values by reference. -If you would like to avoid touching your values, clone them: `jsonpatch.applyPatch(document, jsonpatch.deepClone(patch))`. +If you would like to avoid touching your `patch` array values, clone them: `jsonpatch.applyPatch(document, jsonpatch.deepClone(patch))`. Returns an array of [`OperationResult`](#operationresult-type) objects - one item for each item in `patches`, each item is an object `{newDocument: any, test?: boolean, removed?: any}`. @@ -197,13 +203,13 @@ Returns an array of [`OperationResult`](#operationresult-type) objects - one ite * `remove`, `replace` and `move` - original object that has been removed * `add` (only when adding to an array) - index at which item has been inserted (useful when using `-` alias) -** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** - -** Note II: the returned array has `newDocument` property that you can use as the final state of the patched document **. +- ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** +- ** Note II: the returned array has `newDocument` property that you can use as the final state of the patched document **. +- ** Note III: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. - See [Validation notes](#validation-notes). -#### `applyOperation(document: any, operation: Operation, validateOperation: = false, mutateDocument = true): OperationResult` +#### `function applyOperation(document: T, operation: Operation, validateOperation: boolean | Validator = false, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): OperationResult` Applies single operation object `operation` on `document`. @@ -211,13 +217,15 @@ Applies single operation object `operation` on `document`. - `operation` The operation to apply - `validateOperation` Whether to validate the operation, or to pass a validator callback - `mutateDocument` Whether to mutate the original document or clone it before applying +- `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. It modifies the `document` object and `operation` - it gets the values by reference. If you would like to avoid touching your values, clone them: `jsonpatch.applyOperation(document, jsonpatch.deepClone(operation))`. Returns an [`OperationResult`](#operationresult-type) object `{newDocument: any, test?: boolean, removed?: any}`. -** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** +- ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** +- ** Note II: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. - See [Validation notes](#validation-notes). diff --git a/dist/fast-json-patch.js b/dist/fast-json-patch.js index 945d3f53..991829f1 100644 --- a/dist/fast-json-patch.js +++ b/dist/fast-json-patch.js @@ -347,11 +347,13 @@ exports.getValueByPointer = getValueByPointer; * @param operation The operation to apply * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. * @param mutateDocument Whether to mutate the original document or clone it before applying + * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. * @return `{newDocument, result}` after the operation */ -function applyOperation(document, operation, validateOperation, mutateDocument) { +function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications) { if (validateOperation === void 0) { validateOperation = false; } if (mutateDocument === void 0) { mutateDocument = true; } + if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } if (validateOperation) { if (typeof validateOperation == 'function') { validateOperation(operation, 0, document, operation.path); @@ -425,6 +427,9 @@ function applyOperation(document, operation, validateOperation, mutateDocument) } while (true) { key = keys[t]; + if (banPrototypeModifications && key == '__proto__') { + throw new TypeError('JSON-Patch: modifying `__proto_` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'); + } if (validateOperation) { if (existingPathFragment === undefined) { if (obj[key] === undefined) { @@ -490,10 +495,12 @@ exports.applyOperation = applyOperation; * @param patch The patch to apply * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. * @param mutateDocument Whether to mutate the original document or clone it before applying + * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. * @return An array of `{newDocument, result}` after the patch */ -function applyPatch(document, patch, validateOperation, mutateDocument) { +function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) { if (mutateDocument === void 0) { mutateDocument = true; } + if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } if (validateOperation) { if (!Array.isArray(patch)) { throw new exports.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); @@ -504,7 +511,7 @@ function applyPatch(document, patch, validateOperation, mutateDocument) { } var results = new Array(patch.length); for (var i = 0, length_1 = patch.length; i < length_1; i++) { - results[i] = applyOperation(document, patch[i], validateOperation); + results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications); document = results[i].newDocument; // in case root was replaced } results.newDocument = document; diff --git a/dist/fast-json-patch.min.js b/dist/fast-json-patch.min.js index ec817fb1..6e7399f5 100644 --- a/dist/fast-json-patch.min.js +++ b/dist/fast-json-patch.min.js @@ -1,2 +1,2 @@ /*! fast-json-patch, version: 2.0.7 */ -var jsonpatch=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.i=function(a){return a},b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=3)}([function(a,b){function c(a,b){return i.call(a,b)}function d(a){if(Array.isArray(a)){for(var b=Array(a.length),d=0;d=b){c++;continue}return!1}return!0},b.escapePathComponent=e,b.unescapePathComponent=function(a){return a.replace(/~1/g,'/').replace(/~0/g,'~')},b._getPathRecursive=f,b.getPath=function(a,b){if(a===b)return'/';var c=f(a,b);if(''===c)throw new Error('Object not found in root');return'/'+c},b.hasUndefined=g;var j=function(a){function b(b,c,d,e,f){a.call(this,b),this.message=b,this.name=c,this.index=d,this.operation=e,this.tree=f}return h(b,a),b}(Error);b.PatchError=j},function(a,b,c){function d(a,b){if(''==b)return a;var c={op:'_get',path:b};return e(a,c),c.value}function e(a,c,e,f){if(void 0===e&&(e=!1),void 0===f&&(f=!0),e&&('function'==typeof e?e(c,0,a,c.path):g(c,0)),''===c.path){var h={newDocument:a};if('add'===c.op)return h.newDocument=c.value,h;if('replace'===c.op)return h.newDocument=c.value,h.removed=a,h;if('move'===c.op||'copy'===c.op)return h.newDocument=d(a,c.from),'move'===c.op&&(h.removed=a),h;if('test'===c.op){if(h.test=k(a,c.value),!1===h.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return h.newDocument=a,h}if('remove'===c.op)return h.removed=a,h.newDocument=null,h;if('_get'===c.op)return c.value=a,h;if(e)throw new b.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902','OPERATION_OP_INVALID',0,c,a);else return h}else{f||(a=l._deepClone(a));var i,j,o,p=c.path||'',q=p.split('/'),r=a,s=1,t=q.length;for(o='function'==typeof e?e:g;;){if(j=q[s],e&&void 0==i&&(void 0===r[j]?i=q.slice(0,s).join('/'):s==t-1&&(i=c.path),void 0!==i&&o(c,0,a,i)),s++,Array.isArray(r)){if('-'===j)j=r.length;else if(e&&!l.isInteger(j))throw new b.JsonPatchError('Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index','OPERATION_PATH_ILLEGAL_ARRAY_INDEX',0,c.path,c);else l.isInteger(j)&&(j=~~j);if(s>=t){if(e&&'add'===c.op&&j>r.length)throw new b.JsonPatchError('The specified index MUST NOT be greater than the number of elements in the array','OPERATION_VALUE_OUT_OF_BOUNDS',0,c.path,c);var h=n[c.op].call(c,r,j,a);if(!1===h.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return h}}else if(j&&-1!=j.indexOf('~')&&(j=l.unescapePathComponent(j)),s>=t){var h=m[c.op].call(c,r,j,a);if(!1===h.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return h}r=r[j]}}}function f(a,c,d,f){if(void 0===f&&(f=!0),d&&!Array.isArray(c))throw new b.JsonPatchError('Patch sequence must be an array','SEQUENCE_NOT_AN_ARRAY');f||(a=l._deepClone(a));for(var g=Array(c.length),h=0,i=c.length;h=b){c++;continue}return!1}return!0},b.escapePathComponent=e,b.unescapePathComponent=function(a){return a.replace(/~1/g,'/').replace(/~0/g,'~')},b._getPathRecursive=f,b.getPath=function(a,b){if(a===b)return'/';var c=f(a,b);if(''===c)throw new Error('Object not found in root');return'/'+c},b.hasUndefined=g;var j=function(a){function b(b,c,d,e,f){a.call(this,b),this.message=b,this.name=c,this.index=d,this.operation=e,this.tree=f}return h(b,a),b}(Error);b.PatchError=j},function(a,b,c){function d(a,b){if(''==b)return a;var c={op:'_get',path:b};return e(a,c),c.value}function e(a,c,e,f,h){if(void 0===e&&(e=!1),void 0===f&&(f=!0),void 0===h&&(h=!0),e&&('function'==typeof e?e(c,0,a,c.path):g(c,0)),''===c.path){var i={newDocument:a};if('add'===c.op)return i.newDocument=c.value,i;if('replace'===c.op)return i.newDocument=c.value,i.removed=a,i;if('move'===c.op||'copy'===c.op)return i.newDocument=d(a,c.from),'move'===c.op&&(i.removed=a),i;if('test'===c.op){if(i.test=k(a,c.value),!1===i.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return i.newDocument=a,i}if('remove'===c.op)return i.removed=a,i.newDocument=null,i;if('_get'===c.op)return c.value=a,i;if(e)throw new b.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902','OPERATION_OP_INVALID',0,c,a);else return i}else{f||(a=l._deepClone(a));var j,o,p,q=c.path||'',r=q.split('/'),s=a,u=1,t=r.length;for(p='function'==typeof e?e:g;;){if(o=r[u],h&&'__proto__'==o)throw new TypeError('JSON-Patch: modifying `__proto_` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README');if(e&&void 0==j&&(void 0===s[o]?j=r.slice(0,u).join('/'):u==t-1&&(j=c.path),void 0!==j&&p(c,0,a,j)),u++,Array.isArray(s)){if('-'===o)o=s.length;else if(e&&!l.isInteger(o))throw new b.JsonPatchError('Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index','OPERATION_PATH_ILLEGAL_ARRAY_INDEX',0,c.path,c);else l.isInteger(o)&&(o=~~o);if(u>=t){if(e&&'add'===c.op&&o>s.length)throw new b.JsonPatchError('The specified index MUST NOT be greater than the number of elements in the array','OPERATION_VALUE_OUT_OF_BOUNDS',0,c.path,c);var i=n[c.op].call(c,s,o,a);if(!1===i.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return i}}else if(o&&-1!=o.indexOf('~')&&(o=l.unescapePathComponent(o)),u>=t){var i=m[c.op].call(c,s,o,a);if(!1===i.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',0,c,a);return i}s=s[o]}}}function f(a,c,d,f,g){if(void 0===f&&(f=!0),void 0===g&&(g=!0),d&&!Array.isArray(c))throw new b.JsonPatchError('Patch sequence must be an array','SEQUENCE_NOT_AN_ARRAY');f||(a=l._deepClone(a));for(var h=Array(c.length),j=0,i=c.length;j(document: T, operation: Operation, validateOperation: boolean | Validator = false, mutateDocument: boolean = true): OperationResult { +export function applyOperation(document: T, operation: Operation, validateOperation: boolean | Validator = false, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): OperationResult { if (validateOperation) { if (typeof validateOperation == 'function') { validateOperation(operation, 0, document, operation.path); @@ -264,6 +265,10 @@ export function applyOperation(document: T, operation: Operation, validateOpe while (true) { key = keys[t]; + if(banPrototypeModifications && key == '__proto__') { + throw new TypeError('JSON-Patch: modifying `__proto_` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'); + } + if (validateOperation) { if (existingPathFragment === undefined) { if (obj[key] === undefined) { @@ -329,9 +334,10 @@ export function applyOperation(document: T, operation: Operation, validateOpe * @param patch The patch to apply * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. * @param mutateDocument Whether to mutate the original document or clone it before applying + * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. * @return An array of `{newDocument, result}` after the patch */ -export function applyPatch(document: T, patch: Operation[], validateOperation?: boolean | Validator, mutateDocument: boolean = true): PatchResult { +export function applyPatch(document: T, patch: Operation[], validateOperation?: boolean | Validator, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): PatchResult { if(validateOperation) { if(!Array.isArray(patch)) { throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); @@ -343,7 +349,7 @@ export function applyPatch(document: T, patch: Operation[], validateOperation const results = new Array(patch.length) as PatchResult; for (let i = 0, length = patch.length; i < length; i++) { - results[i] = applyOperation(document, patch[i], validateOperation); + results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications); document = results[i].newDocument; // in case root was replaced } results.newDocument = document; diff --git a/test/spec/coreSpec.js b/test/spec/coreSpec.js index dcabb0c9..369005eb 100644 --- a/test/spec/coreSpec.js +++ b/test/spec/coreSpec.js @@ -9,40 +9,44 @@ if (typeof _ === 'undefined') { describe('jsonpatch.getValueByPointer', function() { it('should retrieve values by JSON pointer from tree - deep object', function() { var obj = { - person: {name: 'Marilyn'} + person: { name: 'Marilyn' } }; - var name = jsonpatch.getValueByPointer(obj, '/person/name') + var name = jsonpatch.getValueByPointer(obj, '/person/name'); expect(name).toEqual('Marilyn'); }); it('should retrieve values by JSON pointer from tree - deep array', function() { var obj = { - people: [{name: 'Marilyn'}, {name: 'Monroe'}] + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] }; - var name = jsonpatch.getValueByPointer(obj, '/people/1/name') + var name = jsonpatch.getValueByPointer(obj, '/people/1/name'); expect(name).toEqual('Monroe'); }); it('should retrieve values by JSON pointer from tree - root object', function() { var obj = { - people: [{name: 'Marilyn'}, {name: 'Monroe'}] + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] }; var retrievedObject = jsonpatch.getValueByPointer(obj, ''); expect(retrievedObject).toEqual({ - people: [{name: 'Marilyn'}, {name: 'Monroe'}] + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] }); }); it('should retrieve values by JSON pointer from tree - root array', function() { - var obj = [{ - people: [{name: 'Marilyn'}, {name: 'Monroe'}] - }]; + var obj = [ + { + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] + } + ]; var retrievedObject = jsonpatch.getValueByPointer(obj, ''); - expect(retrievedObject).toEqual([{ - people: [{name: 'Marilyn'}, {name: 'Monroe'}] - }]); + expect(retrievedObject).toEqual([ + { + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] + } + ]); }); }); describe('jsonpatch.applyReducer - using with Array#reduce', function() { @@ -62,26 +66,30 @@ describe('jsonpatch.applyReducer - using with Array#reduce', function() { }); }); describe('root replacement with applyOperation', function() { - describe('_get operation', function () { + describe('_get operation', function() { it('should get root value', function() { - var obj = [{ - people: [{name: 'Marilyn'}, {name: 'Monroe'}] - }]; + var obj = [ + { + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] + } + ]; - var patch = {op: '_get', path: ''}; + var patch = { op: '_get', path: '' }; jsonpatch.applyOperation(obj, patch); - expect(patch.value).toEqual([{ - people: [{name: 'Marilyn'}, {name: 'Monroe'}] - }]); + expect(patch.value).toEqual([ + { + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] + } + ]); }); it('should get deep value', function() { var obj = { - people: [{name: 'Marilyn'}, {name: 'Monroe'}] + people: [{ name: 'Marilyn' }, { name: 'Monroe' }] }; - var patch = {op: '_get', path: '/people/1/name'}; + var patch = { op: '_get', path: '/people/1/name' }; jsonpatch.applyOperation(obj, patch); @@ -179,7 +187,7 @@ describe('root replacement with applyOperation', function() { } ]); }); - it('should `add` an array prop', function() { + it('should `add` an array prop', function() { var obj = []; var newObj = jsonpatch.applyOperation(obj, { @@ -1508,25 +1516,23 @@ describe('core', function() { it('should apply copy, without leaving cross-reference between nodes', function() { var obj = {}; var patchset = [ - {op: 'add', path: '/foo', value: []}, - {op: 'add', path: '/foo/-', value: 1}, - {op: 'copy', from: '/foo', path: '/bar'}, - {op: 'add', path: '/bar/-', value: 2} + { op: 'add', path: '/foo', value: [] }, + { op: 'add', path: '/foo/-', value: 1 }, + { op: 'copy', from: '/foo', path: '/bar' }, + { op: 'add', path: '/bar/-', value: 2 } ]; jsonpatch.applyPatch(obj, patchset); expect(obj).toEqual({ - "foo": [1], - "bar": [1, 2], + foo: [1], + bar: [1, 2] }); }); - it('should use value object as a reference', function () { + it('should use value object as a reference', function() { var obj1 = {}; - var patch = [ - {op: 'add', path: '/foo', value: []} - ]; + var patch = [{ op: 'add', path: '/foo', value: [] }]; jsonpatch.applyPatch(obj1, patch, false); @@ -1910,5 +1916,56 @@ describe('undefined - JS to JSON projection / JSON to JS extension', function() bar: null }); }); + + it(`should allow __proto__ modifications when the flag is set`, function() { + function SomeClass() { + this.foo = 'bar'; + } + + let doc = new SomeClass(); + let otherDoc = new SomeClass(); + + const patch = [ + { op: 'replace', path: `/__proto__/x`, value: 'polluted' } + ]; + + jsonpatch.applyPatch(doc, patch, false, true, false); + + expect(otherDoc.x).toEqual('polluted'); + }); + + it(`should not allow __proto__ modifications without setting the flag and should throw an error`, function() { + const expectedErrorMessage = + 'JSON-Patch: modifying `__proto_` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'; + + function SomeClass() { + this.foo = 'bar'; + } + + let doc = new SomeClass(); + let otherDoc = new SomeClass(); + + const patch = [ + { op: 'replace', path: `/__proto__/x`, value: 'polluted' } + ]; + + try { + jsonpatch.applyPatch(doc, patch); + } catch (e) { + expect(e.message).toEqual(expectedErrorMessage); + } + + expect(otherDoc.x).toEqual(undefined); + expect(doc.x).toEqual(undefined); + + let arr = []; + + try { + jsonpatch.applyPatch(arr, patch); + } catch (e) { + expect(e.message).toEqual(expectedErrorMessage); + } + expect(arr.x).toEqual(undefined); + }); }); });