CVE-2020-28270: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: Prototype pollution vulnerability in 'object-hierarchy-access' versions 0.2.0 through 0.32.0 allows attacker to cause a denial of service and may lead to remote code execution.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2020-11-12T18:15Z
Repository: https://github.com/mjpclab/object-hierarchy-access
Language: JavaScript
Files changed: built/setup.js, src/setup.ts, test/set/7-no-proto-polution.js, test/set/index.js
Commit date: 2020-11-07 17:09:38 +0800
Commit message: fix(setup): prevent assigning value to `__proto__`

Assigning value to `__proto__` polutes `Object.prototype`.

Reported by Daniel Elkabes from White Source Software.

Diff: diff --git a/built/setup.js b/built/setup.js
index 21cbf43..940f3a7 100644
--- a/built/setup.js
+++ b/built/setup.js
@@ -1,14 +1,19 @@
 import { normalizeDescriptor } from './utility/setup';
 import { getNonEmptyPropName } from './utility/common';
+const propProto = '__proto__';
 function generate(target, hierarchies, forceOverride) {
     let current = target;
     hierarchies.forEach(info => {
         const descriptor = normalizeDescriptor(info);
         const { value, type, create, override, created, skipped, got } = descriptor;
         const name = getNonEmptyPropName(current, descriptor);
-        if (forceOverride || override || !current[name] || typeof current[name] !== 'object') {
+        if (forceOverride ||
+            override ||
+            !current[name] ||
+            typeof current[name] !== 'object' ||
+            (name === propProto && current[name] === Object.prototype)) {
             const obj = value ? value :
                 type ? new type() :
                     create ? create.call(current, current, name) :
                         {};
             current[name] = obj;
diff --git a/dist/index.esm.js b/dist/index.esm.js
index 021a1d8..f5bc360 100644
Binary files a/dist/index.esm.js and b/dist/index.esm.js differ
diff --git a/dist/index.js b/dist/index.js
index d62e86c..4c87124 100644
Binary files a/dist/index.js and b/dist/index.js differ
diff --git a/dist/index.min.js b/dist/index.min.js
index 0cb0cb9..6b1eda4 100644
Binary files a/dist/index.min.js and b/dist/index.min.js differ
diff --git a/src/setup.ts b/src/setup.ts
index 7432c0f..42fe536 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -1,9 +1,11 @@
 import type {SetupPropParam} from './type';
 import {normalizeDescriptor} from './utility/setup';
 import {getNonEmptyPropName} from './utility/common';
 
+const propProto = '__proto__';
+
 function generate(
 	target: any,
 	hierarchies: SetupPropParam[],
 	forceOverride?: boolean
 ) {
@@ -11,11 +13,17 @@ function generate(
 	hierarchies.forEach(info => {
 		const descriptor = normalizeDescriptor(info);
 		const {value, type, create, override, created, skipped, got} = descriptor;
 
 		const name = getNonEmptyPropName(current, descriptor);
-		if (forceOverride || override || !current[name] || typeof current[name] !== 'object') {
+		if (
+			forceOverride ||
+			override ||
+			!current[name] ||
+			typeof current[name] !== 'object' ||
+			(name === propProto && current[name] === Object.prototype)
+		) {
 			const obj = value ? value :
 				type ? new type() :
 					create ? create.call(current, current, name) :
 						{};
 			current[name] = obj;
diff --git a/test/set/7-no-proto-polution.js b/test/set/7-no-proto-polution.js
new file mode 100644
index 0000000..0894eb7
--- /dev/null
+++ b/test/set/7-no-proto-polution.js
@@ -0,0 +1,9 @@
+const assert = require('assert').strict;
+const {set} = require('../../');
+
+const obj1 = {};
+const obj2 = {};
+set(obj1, '__proto__', 'admin', true);
+assert.notEqual(obj1.__proto__, Object.prototype);
+assert.equal(obj1.__proto__.admin, true);
+assert.strictEqual(obj2.admin, undefined);
diff --git a/test/set/index.js b/test/set/index.js
index 1b1cf63..6cf5072 100644
--- a/test/set/index.js
+++ b/test/set/index.js
@@ -2,5 +2,6 @@ require('./1-normal');
 require('./2-with-init');
 require('./3-symbol-property');
 require('./4-array-params');
 require('./5-array-value');
 require('./6-option-params');
+require('./7-no-proto-polution');

Vulnerable code:         if (forceOverride || override || !current[name] || typeof current[name] !== 'object') {
		if (forceOverride || override || !current[name] || typeof current[name] !== 'object') {
Fixed code: const propProto = '__proto__';
        if (forceOverride ||
            override ||
            !current[name] ||
            typeof current[name] !== 'object' ||
            (name === propProto && current[name] === Object.prototype)) {
const propProto = '__proto__';

		if (
			forceOverride ||
			override ||
			!current[name] ||
			typeof current[name] !== 'object' ||
			(name === propProto && current[name] === Object.prototype)
		) {
const assert = require('assert').strict;
const {set} = require('../../');

const obj1 = {};
const obj2 = {};
set(obj1, '__proto__', 'admin', true);
assert.notEqual(obj1.__proto__, Object.prototype);
assert.equal(obj1.__proto__.admin, true);
assert.strictEqual(obj2.admin, undefined);
require('./7-no-proto-polution');