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/.travis.yml b/.travis.yml index 2c24b7d..d2ba1bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ sudo: false language: node_js node_js: + - '14' + - '13' + - '12' + - '11' - '10' - '9' - '8' @@ -13,7 +17,15 @@ node_js: - '0.12' - '0.10' -after_success: 'npm run coveralls' +before_install: + - if [ $(echo "${TRAVIS_NODE_VERSION}" | cut -d'.' -f1) -eq 5 ]; then + npm i -g npm@4; + fi + +after_success: + - if [ $(echo "${TRAVIS_NODE_VERSION}" | cut -d'.' -f1) -ge 6 ]; then + npm run coveralls; + fi os: - linux diff --git a/LICENSE b/LICENSE index 39223b4..4e46471 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Takayuki Sato +Copyright (c) 2016-2021 Gulp Team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8fe18d3..b38f2b1 100644 --- a/README.md +++ b/README.md @@ -209,19 +209,19 @@ If *converter* is given, it is able to convert the terminal values. ## License -Copyright (C) 2016-2018 Takayuki Sato +Copyright (C) 2016-2021 Gulp Team. This program is free software under [MIT][mit-url] License. See the file LICENSE in this distribution for more details. -[repo-url]: https://github.com/sttk/copy-props/ -[npm-img]: https://img.shields.io/badge/npm-v2.0.4-blue.svg +[repo-url]: https://github.com/gulpjs/copy-props/ +[npm-img]: https://img.shields.io/badge/npm-v2.0.5-blue.svg [npm-url]: https://www.npmjs.org/package/copy-props/ [mit-img]: https://img.shields.io/badge/license-MIT-green.svg [mit-url]: https://opensource.org/licenses.MIT -[travis-img]: https://travis-ci.org/sttk/copy-props.svg?branch=master -[travis-url]: https://travis-ci.org/sttk/copy-props -[appveyor-img]: https://ci.appveyor.com/api/projects/status/github/sttk/copy-props?branch=master&svg=true -[appveyor-url]: https://ci.appveyor.com/project/sttk/copy-props -[coverage-img]: https://coveralls.io/repos/github/sttk/copy-props/badge.svg?branch=master -[coverage-url]: https://coveralls.io/github/sttk/copy-props?branch=master +[travis-img]: https://travis-ci.org/gulpjs/copy-props.svg?branch=master +[travis-url]: https://travis-ci.org/gulpjs/copy-props +[appveyor-img]: https://ci.appveyor.com/api/projects/status/github/gulpjs/copy-props?branch=master&svg=true +[appveyor-url]: https://ci.appveyor.com/project/gulpjs/copy-props +[coverage-img]: https://coveralls.io/repos/github/gulpjs/copy-props/badge.svg?branch=master +[coverage-url]: https://coveralls.io/github/gulpjs/copy-props?branch=master diff --git a/appveyor.yml b/appveyor.yml index c7b5123..95737c3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,10 @@ environment: matrix: # node.js + - nodejs_version: "14" + - nodejs_version: "13" + - nodejs_version: "12" + - nodejs_version: "11" - nodejs_version: "10" - nodejs_version: "9" - nodejs_version: "8" @@ -16,7 +20,8 @@ environment: install: - ps: Install-Product node $env:nodejs_version - - npm install + - ps: if ($env:nodejs_version -eq '5') { npm i -g npm@4 } + - cmd: npm install test_script: - node --version diff --git a/index.js b/index.js index 633c8fb..994807e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ 'use strict'; var eachProps = require('each-props'); -var isPlainObject = require('is-plain-object'); +var isPlainObject = require('is-plain-object').isPlainObject; module.exports = function(src, dst, fromto, converter, reverse) { @@ -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 db327c8..6b2a51e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "copy-props", - "version": "2.0.4", + "version": "2.0.5", "description": "Copy properties deeply between two objects.", "main": "index.js", "files": [ @@ -18,7 +18,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/sttk/copy-props.git" + "url": "git+https://github.com/gulpjs/copy-props.git" }, "keywords": [ "object", @@ -28,23 +28,24 @@ "map", "convert" ], - "author": "Takayuki Sato", + "author": "Gulp Team (https://gulpjs.com/)", "license": "MIT", "bugs": { - "url": "https://github.com/sttk/copy-props/issues" + "url": "https://github.com/gulpjs/copy-props/issues" }, - "homepage": "https://github.com/sttk/copy-props#readme", + "homepage": "https://github.com/gulpjs/copy-props#readme", "dependencies": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" }, "devDependencies": { - "browserify": "^16.2.2", + "browserify": "^16.5.2", "chai": "^3.5.0", - "coveralls": "^3.0.1", - "eslint": "^4.19.1", - "mocha": "^3.2.0", - "nyc": "^11.7.2", - "uglify-js": "^3.3.24" + "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 0763a14..be9a231 100644 --- a/test/copy-props-proc.js +++ b/test/copy-props-proc.js @@ -4,7 +4,7 @@ var copyProps = require('..'); var chai = require('chai'); var expect = chai.expect; -/* eslint max-statements: "off", branch-style: "off" */ +/* eslint max-statements: "off" */ describe('Processing', function() { @@ -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 0f06128..815e42f 100644 --- a/test/web/copy-props-proc.js +++ b/test/web/copy-props-proc.js @@ -4,7 +4,7 @@ var expect = chai.expect; -/* eslint max-statements: "off", branch-style: "off" */ +/* eslint max-statements: "off" */ describe('Processing', function() { @@ -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 19d7948..31337a3 100644 --- a/web/copy-props.js +++ b/web/copy-props.js @@ -2,7 +2,7 @@ 'use strict'; var eachProps = require('each-props'); -var isPlainObject = require('is-plain-object'); +var isPlainObject = require('is-plain-object').isPlainObject; module.exports = function(src, dst, fromto, converter, reverse) { @@ -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,7 +230,11 @@ function isObject(v) { return Object.prototype.toString.call(v) === '[object Object]'; } -},{"each-props":4,"is-plain-object":7}],2:[function(require,module,exports){ +function isPossibilityOfPrototypePollution(key) { + return (key === '__proto__' || key === 'constructor'); +} + +},{"each-props":4,"is-plain-object":8}],2:[function(require,module,exports){ /*! * array-each * @@ -368,7 +376,46 @@ function isObject(v) { } -},{"is-plain-object":7,"object.defaults/immutable":9}],5:[function(require,module,exports){ +},{"is-plain-object":5,"object.defaults/immutable":10}],5:[function(require,module,exports){ +/*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + +'use strict'; + +var isObject = require('isobject'); + +function isObjectObject(o) { + return isObject(o) === true + && Object.prototype.toString.call(o) === '[object Object]'; +} + +module.exports = function isPlainObject(o) { + var ctor,prot; + + if (isObjectObject(o) === false) return false; + + // If has modified constructor + ctor = o.constructor; + if (typeof ctor !== 'function') return false; + + // If has modified prototype + prot = ctor.prototype; + if (isObjectObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; +}; + +},{"isobject":9}],6:[function(require,module,exports){ /*! * for-in * @@ -386,7 +433,7 @@ module.exports = function forIn(obj, fn, thisArg) { } }; -},{}],6:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ /*! * for-own * @@ -407,7 +454,11 @@ module.exports = function forOwn(obj, fn, thisArg) { }); }; -},{"for-in":5}],7:[function(require,module,exports){ +},{"for-in":6}],8:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + /*! * is-plain-object * @@ -415,27 +466,22 @@ module.exports = function forOwn(obj, fn, thisArg) { * Released under the MIT License. */ -'use strict'; - -var isObject = require('isobject'); - -function isObjectObject(o) { - return isObject(o) === true - && Object.prototype.toString.call(o) === '[object Object]'; +function isObject(o) { + return Object.prototype.toString.call(o) === '[object Object]'; } -module.exports = function isPlainObject(o) { +function isPlainObject(o) { var ctor,prot; - if (isObjectObject(o) === false) return false; + if (isObject(o) === false) return false; // If has modified constructor ctor = o.constructor; - if (typeof ctor !== 'function') return false; + if (ctor === undefined) return true; // If has modified prototype prot = ctor.prototype; - if (isObjectObject(prot) === false) return false; + if (isObject(prot) === false) return false; // If constructor does not have an Object-specific method if (prot.hasOwnProperty('isPrototypeOf') === false) { @@ -444,9 +490,11 @@ module.exports = function isPlainObject(o) { // Most likely a plain Object return true; -}; +} -},{"isobject":8}],8:[function(require,module,exports){ +exports.isPlainObject = isPlainObject; + +},{}],9:[function(require,module,exports){ /*! * isobject * @@ -460,7 +508,7 @@ module.exports = function isObject(val) { return val != null && typeof val === 'object' && Array.isArray(val) === false; }; -},{}],9:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ 'use strict'; var slice = require('array-slice'); @@ -482,7 +530,7 @@ module.exports = function immutableDefaults() { return defaults.apply(null, [{}].concat(args)); }; -},{"./mutable":10,"array-slice":3}],10:[function(require,module,exports){ +},{"./mutable":11,"array-slice":3}],11:[function(require,module,exports){ 'use strict'; var each = require('array-each'); @@ -519,5 +567,5 @@ module.exports = function defaults(target, objects) { return target; }; -},{"array-each":2,"array-slice":3,"for-own":6,"isobject":8}]},{},[1])(1) +},{"array-each":2,"array-slice":3,"for-own":7,"isobject":9}]},{},[1])(1) }); diff --git a/web/copy-props.min.js b/web/copy-props.min.js index ecc3950..bd809c7 100644 --- a/web/copy-props.min.js +++ b/web/copy-props.min.js @@ -1,2 +1,2 @@ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).copyProps=t()}}(function(){return function i(f,u,a){function c(r,t){if(!u[r]){if(!f[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=u[r]={exports:{}};f[r][0].call(o.exports,function(t){return c(f[r][1][t]||t)},o,o.exports,i,f,u,a)}return u[r].exports}for(var s="function"==typeof require&&require,t=0;t