CVE-2021-21277: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: angular-expressions is "angular's nicest part extracted as a standalone module for the browser and node". In angular-expressions before version 1.1.2 there is a vulnerability which allows Remote Code Execution if you call "expressions.compile(userControlledInput)" where "userControlledInput" is text that comes from user input. The security of the package could be bypassed by using a more complex payload, using a ".constructor.constructor" technique. In terms of impact: If running angular-expressions in the browser, an attacker could run any browser script when the application code calls expressions.compile(userControlledInput). If running angular-expressions on the server, an attacker could run any Javascript expression, thus gaining Remote Code Execution. This is fixed in version 1.1.2 of angular-expressions A temporary workaround might be either to disable user-controlled input that will be fed into angular-expressions in your application or allow only following characters in the userControlledInput.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 8.8
Published: 2021-02-01T15:15Z
Repository: https://github.com/peerigon/angular-expressions
Language: JavaScript
Files changed: CHANGELOG.md, lib/parse.js, test/main.test.js
Commit date: 2021-01-29 11:36:19 +0100
Commit message: Fix Security Vulnerability by using hasOwnProperty defensively

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 734e96d..7a59930 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
+### 1.1.2
+
+- Disallow access to prototype chain (CVE-2021-21277)
+
 ### 1.1.1
 
 Previous version was published with ES6 feature, now the published JS uses ES5 only
 
 ### 1.1.0
@@ -16,11 +20,11 @@ function validChars(ch) {
     "ÀÈÌÒÙàèìòùÁÉÍÓÚáéíóúÂÊÎÔÛâêîôûÃÑÕãñõÄËÏÖÜŸäëïöüÿß".indexOf(ch) !== -1
   );
 }
 evaluate = compile("être_embarassé", {
   isIdentifierStart: validChars,
-  isIdentifierContinue: validChars
+  isIdentifierContinue: validChars,
 });
 
 evaluate({ être_embarassé: "Ping" });
 ```
 
diff --git a/lib/parse.js b/lib/parse.js
index 0a73fc4..f16dd7e 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -1692,48 +1692,10 @@ var isAutoBootstrapAllowed = allowAutoBootstrap(window.document);
        margin-bottom: 0;
    }
    </file>
  </example>
  */
-function angularInit(element, bootstrap) {
-	var appElement,
-		module,
-		config = {};
-
-	// The element `element` has priority over any other element.
-	forEach(ngAttrPrefixes, function (prefix) {
-		var name = prefix + "app";
-
-		if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
-			appElement = element;
-			module = element.getAttribute(name);
-		}
-	});
-	forEach(ngAttrPrefixes, function (prefix) {
-		var name = prefix + "app";
-		var candidate;
-
-		if (
-			!appElement &&
-			(candidate = element.querySelector("[" + name.replace(":", "\\:") + "]"))
-		) {
-			appElement = candidate;
-			module = candidate.getAttribute(name);
-		}
-	});
-	if (appElement) {
-		if (!isAutoBootstrapAllowed) {
-			window.console.error(
-				"Angular: disabling automatic bootstrap. <script> protocol indicates " +
-					"an extension, document.location.href does not match."
-			);
-			return;
-		}
-		config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
-		bootstrap(appElement, module ? [module] : [], config);
-	}
-}
 
 /**
  * @ngdoc function
  * @name angular.bootstrap
  * @module ng
@@ -3230,15 +3192,16 @@ ASTCompiler.prototype = {
 		recursionFn = recursionFn || noop;
 		if (!skipWatchIdCheck && isDefined(ast.watchId)) {
 			intoId = intoId || this.nextId();
 			this.if_(
 				"i",
-				this.lazyAssign(intoId, this.computedMember("i", ast.watchId)),
+				this.lazyAssign(intoId, this.unsafeComputedMember("i", ast.watchId)),
 				this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true)
 			);
 			return;
 		}
+
 		switch (ast.type) {
 			case AST.Program:
 				forEach(ast.body, function (expression, pos) {
 					self.recurse(
 						expression.expression,
@@ -3354,15 +3317,24 @@ ASTCompiler.prototype = {
 					ast.object,
 					left,
 					undefined,
 					function () {
 						var member = null;
+						const inAssignment = self.current().inAssignment;
 						if (ast.computed) {
 							right = self.nextId();
-							member = self.computedMember(left, right);
+							if (inAssignment || self.state.computing === "assign") {
+								member = self.unsafeComputedMember(left, right);
+							} else {
+								member = self.computedMember(left, right);
+							}
 						} else {
-							member = self.nonComputedMember(left, ast.property.name);
+							if (inAssignment || self.state.computing === "assign") {
+								member = self.unsafeNonComputedMember(left, ast.property.name);
+							} else {
+								member = self.nonComputedMember(left, ast.property.name);
+							}
 							right = ast.property.name;
 						}
 
 						if (ast.computed) {
 							if (ast.property.type === AST.Literal) {
@@ -3445,11 +3417,17 @@ ASTCompiler.prototype = {
 									);
 								});
 								if (left.name) {
 									var x = self.member(left.context, left.name, left.computed);
 									expression =
-										x + ".call(" + [left.context].concat(args).join(",") + ")";
+										"(" +
+										x +
+										" === null ? null : " +
+										self.unsafeMember(left.context, left.name, left.computed) +
+										".call(" +
+										[left.context].concat(args).join(",") +
+										"))";
 								} else {
 									expression = right + "(" + args.join(",") + ")";
 								}
 								self.assign(intoId, expression);
 							},
@@ -3462,10 +3440,11 @@ ASTCompiler.prototype = {
 				}
 				break;
 			case AST.AssignmentExpression:
 				right = this.nextId();
 				left = {};
+				self.current().inAssignment = true;
 				this.recurse(
 					ast.left,
 					undefined,
 					left,
 					function () {
@@ -3487,13 +3466,17 @@ ASTCompiler.prototype = {
 									right;
 								self.assign(intoId, expression);
 								recursionFn(intoId || expression);
 							}
 						);
+						self.current().inAssignment = false;
+						self.recurse(ast.right, right);
+						self.current().inAssignment = true;
 					},
 					1
 				);
+				self.current().inAssignment = false;
 				break;
 			case AST.ArrayExpression:
 				args = [];
 				forEach(ast.elements, function (expr) {
 					self.recurse(
@@ -3530,11 +3513,14 @@ ASTCompiler.prototype = {
 									? property.key.name
 									: "" + property.key.value;
 						}
 						right = self.nextId();
 						self.recurse(property.value, right);
-						self.assign(self.member(intoId, left, property.computed), right);
+						self.assign(
+							self.unsafeMember(intoId, left, property.computed),
+							right
+						);
 					});
 				} else {
 					forEach(ast.properties, function (property) {
 						self.recurse(
 							property.value,
@@ -3664,13 +3650,39 @@ ASTCompiler.prototype = {
 		}
 
 		return expr;
 	},
 
-	computedMember: function (left, right) {
+	unsafeComputedMember: function (left, right) {
 		return left + "[" + right + "]";
 	},
+	unsafeNonComputedMember: function (left, right) {
+		return this.nonComputedMember(left, right);
+	},
+
+	computedMember: function (left, right) {
+		if (this.state.computing === "assign") {
+			return this.unsafeComputedMember(left, right);
+		}
+		// return left + "[" + right + "]";
+		return (
+			"(" +
+			left +
+			".hasOwnProperty(" +
+			right +
+			") ? " +
+			left +
+			"[" +
+			right +
+			"] : null)"
+		);
+	},
+
+	unsafeMember: function (left, right, computed) {
+		if (computed) return this.unsafeComputedMember(left, right);
+		return this.unsafeNonComputedMember(left, right);
+	},
 
 	member: function (left, right, computed) {
 		if (computed) return this.computedMember(left, right);
 		return this.nonComputedMember(left, right);
 	},
@@ -3825,10 +3837,11 @@ ASTInterpreter.prototype = {
 				left = this.recurse(ast.object, false, !!create);
 				if (!ast.computed) {
 					right = ast.property.name;
 				}
 				if (ast.computed) right = this.recurse(ast.property);
+
 				return ast.computed
 					? this.computedMember(left, right, context, create)
 					: this.nonComputedMember(left, right, context, create);
 			case AST.CallExpression:
 				args = [];
diff --git a/test/main.test.js b/test/main.test.js
index 87b0223..df59a26 100644
--- a/test/main.test.js
+++ b/test/main.test.js
@@ -150,10 +150,36 @@ describe("expressions", function () {
 				evaluate = compile("island.pirate.name");
 				expect(evaluate(scope)).to.equal(undefined);
 			});
 		});
 
+		describe("Security", function () {
+			it("should not leak", function () {
+				evaluate = compile(
+					"''['c'+'onstructor']['c'+'onstructor']('return process;')()"
+				);
+				const result = evaluate({});
+				expect(result).to.equal(undefined);
+			});
+
+			it("should not leak indirectly with string concatenation", function () {
+				evaluate = compile(
+					"a = null; a = ''['c'+'onstructor']['c'+'onstructor']; a = a('return process;'); a();"
+				);
+				const result = evaluate({});
+				expect(result).to.equal(undefined);
+			});
+
+			it("should not leak indirectly with literal string", function () {
+				evaluate = compile(
+					"a = null; a = ''['constructor']['constructor']; a = a('return process;'); a();"
+				);
+				const result = evaluate({});
+				expect(result).to.equal(undefined);
+			});
+		});
+
 		describe("when evaluating dot-notated assignments", function () {
 			it("should set the new value on scope", function () {
 				evaluate = compile("island.pirate.name = 'Störtebeker'");
 				evaluate(scope);
 				expect(scope.island.pirate.name).to.equal("Störtebeker");
@@ -466,11 +492,11 @@ describe("expressions", function () {
 			expect(evaluate({})).to.eql(undefined);
 		});
 
 		it("should not leak with computed prop", function () {
 			evaluate = compile("a['split']");
-			expect(evaluate({ a: "" })).to.eql(undefined);
+			expect(evaluate({ a: "" })).to.eql(null);
 		});
 
 		it("should allow to read string length", function () {
 			evaluate = compile("'abc'.length");
 			expect(evaluate({})).to.eql(3);
@@ -485,12 +511,11 @@ describe("expressions", function () {
 		// 	evaluate = compile("name.split = 10");
 		// 	var scope = { name: "hello" };
 		// 	evaluate(scope);
 		// 	expect(scope.name.split).to.be.a("function");
 		// });
-		//
-		//
+
 		it("should work with __proto__", function () {
 			evaluate = compile("__proto__");
 			expect(evaluate({})).to.eql(undefined);
 		});
 

Vulnerable code:   isIdentifierContinue: validChars
function angularInit(element, bootstrap) {
	var appElement,
		module,
		config = {};

	// The element `element` has priority over any other element.
	forEach(ngAttrPrefixes, function (prefix) {
		var name = prefix + "app";

		if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
			appElement = element;
			module = element.getAttribute(name);
		}
	});
	forEach(ngAttrPrefixes, function (prefix) {
		var name = prefix + "app";
		var candidate;

		if (
			!appElement &&
			(candidate = element.querySelector("[" + name.replace(":", "\\:") + "]"))
		) {
			appElement = candidate;
			module = candidate.getAttribute(name);
		}
	});
	if (appElement) {
		if (!isAutoBootstrapAllowed) {
			window.console.error(
				"Angular: disabling automatic bootstrap. <script> protocol indicates " +
					"an extension, document.location.href does not match."
			);
			return;
		}
		config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
		bootstrap(appElement, module ? [module] : [], config);
	}
}
				this.lazyAssign(intoId, this.computedMember("i", ast.watchId)),
							member = self.computedMember(left, right);
							member = self.nonComputedMember(left, ast.property.name);
										x + ".call(" + [left.context].concat(args).join(",") + ")";
						self.assign(self.member(intoId, left, property.computed), right);
	computedMember: function (left, right) {
			expect(evaluate({ a: "" })).to.eql(undefined);
		//
		//
Fixed code: ### 1.1.2

- Disallow access to prototype chain (CVE-2021-21277)

  isIdentifierContinue: validChars,
				this.lazyAssign(intoId, this.unsafeComputedMember("i", ast.watchId)),

						const inAssignment = self.current().inAssignment;
							if (inAssignment || self.state.computing === "assign") {
								member = self.unsafeComputedMember(left, right);
							} else {
								member = self.computedMember(left, right);
							}
							if (inAssignment || self.state.computing === "assign") {
								member = self.unsafeNonComputedMember(left, ast.property.name);
							} else {
								member = self.nonComputedMember(left, ast.property.name);
							}
										"(" +
										x +
										" === null ? null : " +
										self.unsafeMember(left.context, left.name, left.computed) +
										".call(" +
										[left.context].concat(args).join(",") +
										"))";
				self.current().inAssignment = true;
						self.current().inAssignment = false;
						self.recurse(ast.right, right);
						self.current().inAssignment = true;
				self.current().inAssignment = false;
						self.assign(
							self.unsafeMember(intoId, left, property.computed),
							right
						);
	unsafeComputedMember: function (left, right) {
	unsafeNonComputedMember: function (left, right) {
		return this.nonComputedMember(left, right);
	},

	computedMember: function (left, right) {
		if (this.state.computing === "assign") {
			return this.unsafeComputedMember(left, right);
		}
		// return left + "[" + right + "]";
		return (
			"(" +
			left +
			".hasOwnProperty(" +
			right +
			") ? " +
			left +
			"[" +
			right +
			"] : null)"
		);
	},

	unsafeMember: function (left, right, computed) {
		if (computed) return this.unsafeComputedMember(left, right);
		return this.unsafeNonComputedMember(left, right);
	},

		describe("Security", function () {
			it("should not leak", function () {
				evaluate = compile(
					"''['c'+'onstructor']['c'+'onstructor']('return process;')()"
				);
				const result = evaluate({});
				expect(result).to.equal(undefined);
			});

			it("should not leak indirectly with string concatenation", function () {
				evaluate = compile(
					"a = null; a = ''['c'+'onstructor']['c'+'onstructor']; a = a('return process;'); a();"
				);
				const result = evaluate({});
				expect(result).to.equal(undefined);
			});

			it("should not leak indirectly with literal string", function () {
				evaluate = compile(
					"a = null; a = ''['constructor']['constructor']; a = a('return process;'); a();"
				);
				const result = evaluate({});
				expect(result).to.equal(undefined);
			});
		});

			expect(evaluate({ a: "" })).to.eql(null);