Angular Expressions - Remote Code Execution
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.
| Package | Affected versions | Patched versions |
|---|---|---|
angular-expressionsnpm | < 1.1.2 | 1.1.2 |
Affected products
2- Range: < 1.1.2
Patches
107edb62902b1Fix 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- github.com/advisories/GHSA-j6px-jwvv-vpwqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-21277ghsaADVISORY
- blog.angularjs.org/2016/09/angular-16-expression-sandbox-removal.htmlghsax_refsource_MISCWEB
- github.com/peerigon/angular-expressions/commit/07edb62902b1f6127b3dcc013da61c6316dd0bf1ghsax_refsource_MISCWEB
- github.com/peerigon/angular-expressions/security/advisories/GHSA-j6px-jwvv-vpwqghsax_refsource_CONFIRMWEB
- www.npmjs.com/package/angular-expressionsghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.