VYPR
High severityNVD Advisory· Published Feb 1, 2021· Updated Aug 3, 2024

Angular Expressions - Remote Code Execution

CVE-2021-21277

Description

Angular-expressions before 1.1.2 allows remote code execution via a .constructor.constructor payload when user input is passed to expressions.compile().

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Angular-expressions before 1.1.2 allows remote code execution via a .constructor.constructor payload when user input is passed to expressions.compile().

Summary

Angular-expressions before version 1.1.2 contains a vulnerability in expressions.compile() that allows remote code execution if user-controlled input is passed. Attackers can bypass the sandbox using a .constructor.constructor payload, leading to arbitrary JavaScript execution on the client or server. [1][3]

Details

The root cause is insufficient sandboxing in the expression parser. The package extracts Angular's expression compiler for standalone use, but the sandbox can be circumvented by crafting a payload that accesses constructors. [1][3]

Exploitation

An attacker must supply malicious input to the expressions.compile() function. No special privileges are required beyond typical application access. On the server, this leads to full remote code execution; in the browser, arbitrary script execution. [1][3]

Mitigation

Upgrade to version 1.1.2, which fixes the issue. A workaround involves restricting allowed characters in user input, e.g., only alphanumerics and basic operators. [1][3]

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
angular-expressionsnpm
< 1.1.21.1.2

Affected products

2

Patches

1
07edb62902b1

Fix Security Vulnerability by using hasOwnProperty defensively

3 files changed · +90 48
  • CHANGELOG.md+5 1 modified
    @@ -1,3 +1,7 @@
    +### 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
    @@ -18,7 +22,7 @@ function validChars(ch) {
     }
     evaluate = compile("être_embarassé", {
       isIdentifierStart: validChars,
    -  isIdentifierContinue: validChars
    +  isIdentifierContinue: validChars,
     });
     
     evaluate({ être_embarassé: "Ping" });
    
  • lib/parse.js+57 44 modified
    @@ -1694,44 +1694,6 @@ var isAutoBootstrapAllowed = allowAutoBootstrap(window.document);
        </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
    @@ -3232,11 +3194,12 @@ ASTCompiler.prototype = {
     			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) {
    @@ -3356,11 +3319,20 @@ ASTCompiler.prototype = {
     					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;
     						}
     
    @@ -3447,7 +3419,13 @@ 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(",") + ")";
     								}
    @@ -3464,6 +3442,7 @@ ASTCompiler.prototype = {
     			case AST.AssignmentExpression:
     				right = this.nextId();
     				left = {};
    +				self.current().inAssignment = true;
     				this.recurse(
     					ast.left,
     					undefined,
    @@ -3489,9 +3468,13 @@ ASTCompiler.prototype = {
     								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 = [];
    @@ -3532,7 +3515,10 @@ ASTCompiler.prototype = {
     						}
     						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) {
    @@ -3666,9 +3652,35 @@ 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);
    @@ -3827,6 +3839,7 @@ ASTInterpreter.prototype = {
     					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);
    
  • test/main.test.js+28 3 modified
    @@ -152,6 +152,32 @@ describe("expressions", function () {
     			});
     		});
     
    +		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'");
    @@ -468,7 +494,7 @@ describe("expressions", function () {
     
     		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 () {
    @@ -487,8 +513,7 @@ describe("expressions", function () {
     		// 	evaluate(scope);
     		// 	expect(scope.name.split).to.be.a("function");
     		// });
    -		//
    -		//
    +
     		it("should work with __proto__", function () {
     			evaluate = compile("__proto__");
     			expect(evaluate({})).to.eql(undefined);
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.