From 2c738f5c52cfb384b43d977a56a3ab7ce465df9b Mon Sep 17 00:00:00 2001 From: Takayuki Sato Date: Thu, 18 Mar 2021 22:03:00 +0900 Subject: [PATCH] Fix: Avoids prototype pollution (#7) --- .eslintrc | 27 +--------- index.js | 8 +++ package.json | 1 + test/copy-props-proc.js | 102 ++++++++++++++++++++++++++++++++++++ test/web/copy-props-proc.js | 102 ++++++++++++++++++++++++++++++++++++ web/copy-props.js | 8 +++ web/copy-props.min.js | 2 +- web/copy-props.min.js.map | 2 +- 8 files changed, 225 insertions(+), 27 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8880395..76e7721 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,29 +1,6 @@ { - "env": { - "node": true - }, + "extends": "gulp", "rules": { - "array-bracket-spacing": [2, "never"], - "block-scoped-var": 2, - "brace-style": [2, "1tbs"], - "camelcase": 1, - "computed-property-spacing": [2, "never"], - "curly": 2, - "eol-last": 2, - "eqeqeq": [2, "smart"], - "max-depth": [1, 3], - "max-len": [1, 80], - "max-statements": [1, 40], - "new-cap": 1, - "no-extend-native": 2, - "no-mixed-spaces-and-tabs": 2, - "no-trailing-spaces": 2, - "no-unused-vars": 1, - "no-use-before-define": [2, "nofunc"], - "object-curly-spacing": [2, "always"], - "quotes": [2, "single", "avoid-escape"], - "semi": [2, "always"], - "keyword-spacing": [2, { "before": true, "after": true }], - "space-unary-ops": 2 + "max-statements": 0 } } diff --git a/index.js b/index.js index 37bdfdc..994807e 100644 --- a/index.js +++ b/index.js @@ -184,6 +184,10 @@ function setDeep(obj, keyChain, valueCreator) { function _setDeep(obj, keyElems, depth, valueCreator) { var key = keyElems.shift(); + if (isPossibilityOfPrototypePollution(key)) { + return; + } + if (!keyElems.length) { var value = valueCreator(obj, key, depth); if (value === undefined) { @@ -224,3 +228,7 @@ function newUndefined() { function isObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } + +function isPossibilityOfPrototypePollution(key) { + return (key === '__proto__' || key === 'constructor'); +} diff --git a/package.json b/package.json index 85bd124..46848b9 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "chai": "^3.5.0", "coveralls": "^3.1.0", "eslint": "^7.9.0", + "eslint-config-gulp": "^5.0.1", "mocha": "^3.5.3", "nyc": "^15.1.0", "uglify-js": "^3.10.4" diff --git a/test/copy-props-proc.js b/test/copy-props-proc.js index 039dc2e..be9a231 100644 --- a/test/copy-props-proc.js +++ b/test/copy-props-proc.js @@ -701,4 +701,106 @@ describe('Processing', function() { }); + describe('Avoid a prototype pollution vulnerability', function() { + + describe('The critical property key is in a src object', function() { + + it('should ignore a property key: __proto__', function(done) { + var maliciousSrcJson = '{"__proto__":{"polluted":"polluted"},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse(maliciousSrcJson), {})).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype', function(done) { + var maliciousSrcJson = '{"constructor":{"prototype":{"polluted":"polluted"}},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse(maliciousSrcJson), {})).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property key is in a dest object and using reverse', function() { + + it('should ignore a property key: __proto__', function(done) { + var maliciousSrcJson = '{"__proto__":{"polluted":"polluted"},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, JSON.parse(maliciousSrcJson), true)).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype', function(done) { + var maliciousSrcJson = '{"constructor":{"prototype":{"polluted":"polluted"}},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, JSON.parse(maliciousSrcJson), true)).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property value is in a fromto object', function() { + + it('should ignore a property value: __proto__', function(done) { + var fromto = { a: '__proto__.poluuted', b: 'c' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({ a: 'polluted', b: 1 }, {}, fromto)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property value: constructor.prototype', function(done) { + var fromto = { a: 'constructor.prototype.polluted', b: 'c' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({ a: 'polluted', b: 1 }, {}, fromto)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property key is in a fromto object and using reverse', function() { + + it('should ignore a property key: __proto__', function(done) { + var fromto = { '__proto__.poluuted': 'a', c: 'b' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, { a: 'polluted', b: 1 }, fromto, true)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype and using reverse', function(done) { + var fromto = { 'constructor.prototype.polluted': 'a', c: 'b' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, { a: 'polluted', b: 1 }, fromto, true)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical element is in a fromto array', function() { + + it('should ignore an element: __proto__', function(done) { + var fromto = ['__proto__.polluted', 'b']; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse('{"__proto__":{"polluted":"polluted"},"b":1}'), {}, fromto)).to.deep.equal({ b: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore an element: constructor.prototype', function(done) { + var fromto = ['constructor.prototype.polluted', 'b']; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse('{"constructor":{"prototype":{"polluted":"polluted"}},"b":1}'), {}, fromto)).to.deep.equal({ b: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + }); }); diff --git a/test/web/copy-props-proc.js b/test/web/copy-props-proc.js index 40d4585..815e42f 100644 --- a/test/web/copy-props-proc.js +++ b/test/web/copy-props-proc.js @@ -701,4 +701,106 @@ describe('Processing', function() { }); + describe('Avoid a prototype pollution vulnerability', function() { + + describe('The critical property key is in a src object', function() { + + it('should ignore a property key: __proto__', function(done) { + var maliciousSrcJson = '{"__proto__":{"polluted":"polluted"},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse(maliciousSrcJson), {})).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype', function(done) { + var maliciousSrcJson = '{"constructor":{"prototype":{"polluted":"polluted"}},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse(maliciousSrcJson), {})).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property key is in a dest object and using reverse', function() { + + it('should ignore a property key: __proto__', function(done) { + var maliciousSrcJson = '{"__proto__":{"polluted":"polluted"},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, JSON.parse(maliciousSrcJson), true)).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype', function(done) { + var maliciousSrcJson = '{"constructor":{"prototype":{"polluted":"polluted"}},"a":1}'; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, JSON.parse(maliciousSrcJson), true)).to.deep.equal({ a: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property value is in a fromto object', function() { + + it('should ignore a property value: __proto__', function(done) { + var fromto = { a: '__proto__.poluuted', b: 'c' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({ a: 'polluted', b: 1 }, {}, fromto)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property value: constructor.prototype', function(done) { + var fromto = { a: 'constructor.prototype.polluted', b: 'c' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({ a: 'polluted', b: 1 }, {}, fromto)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical property key is in a fromto object and using reverse', function() { + + it('should ignore a property key: __proto__', function(done) { + var fromto = { '__proto__.poluuted': 'a', c: 'b' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, { a: 'polluted', b: 1 }, fromto, true)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore a property key: constructor.prototype and using reverse', function(done) { + var fromto = { 'constructor.prototype.polluted': 'a', c: 'b' }; + expect({}.polluted).to.be.undefined; + expect(copyProps({}, { a: 'polluted', b: 1 }, fromto, true)).to.deep.equal({ c: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + + describe('The critical element is in a fromto array', function() { + + it('should ignore an element: __proto__', function(done) { + var fromto = ['__proto__.polluted', 'b']; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse('{"__proto__":{"polluted":"polluted"},"b":1}'), {}, fromto)).to.deep.equal({ b: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + it('should ignore an element: constructor.prototype', function(done) { + var fromto = ['constructor.prototype.polluted', 'b']; + expect({}.polluted).to.be.undefined; + expect(copyProps(JSON.parse('{"constructor":{"prototype":{"polluted":"polluted"}},"b":1}'), {}, fromto)).to.deep.equal({ b: 1 }); + expect({}.polluted).to.be.undefined; + done(); + }); + + }); + }); }); diff --git a/web/copy-props.js b/web/copy-props.js index 9dd95df..31337a3 100644 --- a/web/copy-props.js +++ b/web/copy-props.js @@ -185,6 +185,10 @@ function setDeep(obj, keyChain, valueCreator) { function _setDeep(obj, keyElems, depth, valueCreator) { var key = keyElems.shift(); + if (isPossibilityOfPrototypePollution(key)) { + return; + } + if (!keyElems.length) { var value = valueCreator(obj, key, depth); if (value === undefined) { @@ -226,6 +230,10 @@ function isObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } +function isPossibilityOfPrototypePollution(key) { + return (key === '__proto__' || key === 'constructor'); +} + },{"each-props":4,"is-plain-object":8}],2:[function(require,module,exports){ /*! * array-each diff --git a/web/copy-props.min.js b/web/copy-props.min.js index 26b2781..bd809c7 100644 --- a/web/copy-props.min.js +++ b/web/copy-props.min.js @@ -1,2 +1,2 @@ -!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).copyProps=t()}(function(){return function i(u,a,c){function f(r,t){if(!a[r]){if(!u[r]){var n="function"==typeof require&&require;if(!t&&n)return n(r,!0);if(s)return s(r,!0);var e=new Error("Cannot find module '"+r+"'");throw e.code="MODULE_NOT_FOUND",e}var o=a[r]={exports:{}};u[r][0].call(o.exports,function(t){return f(u[r][1][t]||t)},o,o.exports,i,u,a,c)}return a[r].exports}for(var s="function"==typeof require&&require,t=0;t