From c77b9d29e4fadbbaf691c83eedc7d2224a2beb0f Mon Sep 17 00:00:00 2001 From: Martin Oppitz Date: Thu, 12 Nov 2020 11:52:51 +0100 Subject: [PATCH] fix: prototype pollution vulnerability + working tests --- index.js | 19 ++++++++++++++----- package.json | 1 + test/index.js | 26 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index f73bbad..d0dc43e 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict'; - /* ! * Chai - pathval utility * Copyright(c) 2012-2014 Jake Luer @@ -77,6 +76,13 @@ function parsePath(path) { var str = path.replace(/([^\\])\[/g, '$1.['); var parts = str.match(/(\\\.|[^.]+?)+/g); return parts.map(function mapMatches(value) { + if ( + value === 'constructor' || + value === '__proto__' || + value === 'prototype' + ) { + return {}; + } var regexp = /^\[(\d+)\]$/; var mArr = regexp.exec(value); var parsed = null; @@ -108,7 +114,7 @@ function parsePath(path) { function internalGetPathValue(obj, parsed, pathDepth) { var temporaryValue = obj; var res = null; - pathDepth = (typeof pathDepth === 'undefined' ? parsed.length : pathDepth); + pathDepth = typeof pathDepth === 'undefined' ? parsed.length : pathDepth; for (var i = 0; i < pathDepth; i++) { var part = parsed[i]; @@ -119,7 +125,7 @@ function internalGetPathValue(obj, parsed, pathDepth) { temporaryValue = temporaryValue[part.p]; } - if (i === (pathDepth - 1)) { + if (i === pathDepth - 1) { res = temporaryValue; } } @@ -153,7 +159,7 @@ function internalSetPathValue(obj, val, parsed) { part = parsed[i]; // If it's the last part of the path, we set the 'propName' value with the property name - if (i === (pathDepth - 1)) { + if (i === pathDepth - 1) { propName = typeof part.p === 'undefined' ? part.i : part.p; // Now we set the property with the name held by 'propName' on object with the desired val tempObj[propName] = val; @@ -200,7 +206,10 @@ function getPathInfo(obj, path) { var parsed = parsePath(path); var last = parsed[parsed.length - 1]; var info = { - parent: parsed.length > 1 ? internalGetPathValue(obj, parsed, parsed.length - 1) : obj, + parent: + parsed.length > 1 ? + internalGetPathValue(obj, parsed, parsed.length - 1) : + obj, name: last.p || last.i, value: internalGetPathValue(obj, parsed), }; diff --git a/package.json b/package.json index f5e0e52..b2c99d3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "scripts": { "build": "browserify --standalone pathval -o pathval.js", "lint": "eslint --ignore-path .gitignore .", + "lint:fix": "npm run lint -- --fix", "prepublish": "npm run build", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "pretest": "npm run lint", diff --git a/test/index.js b/test/index.js index addaad8..5054582 100644 --- a/test/index.js +++ b/test/index.js @@ -218,4 +218,30 @@ describe('setPathValue', function () { var valueReturned = pathval.setPathValue(obj, 'hello[2]', 3); assert(obj === valueReturned); }); + + describe('fix prototype pollution vulnerability', function () { + + it('exclude constructor', function () { + var obj = {}; + assert(typeof obj.constructor === 'function'); // eslint-disable-line + pathval.setPathValue(obj, 'constructor', null); + assert(typeof obj.constructor === 'function'); // eslint-disable-line + }); + + it('exclude __proto__', function () { + var obj = {}; + assert(typeof polluted === 'undefined'); // eslint-disable-line + pathval.setPathValue(obj, '__proto__.polluted', true); + assert(typeof polluted === 'undefined'); // eslint-disable-line + }); + + it('exclude prototype', function () { + var obj = {}; + assert(typeof obj.prototype === 'undefined'); // eslint-disable-line + pathval.setPathValue(obj, 'prototype', true); + assert(typeof obj.prototype === 'undefined'); // eslint-disable-line + }); + + }); + });