Remote Code Execution in Angular Expressions
Description
Angular-expressions before 1.0.1 allows remote code execution via expressions.compile() when attacker-controlled input is used.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Angular-expressions before 1.0.1 allows remote code execution via expressions.compile() when attacker-controlled input is used.
Vulnerability
Description
The angular-expressions library before version 1.0.1 contains a remote code execution (RCE) vulnerability in the expressions.compile() function [1]. The root cause is that the library fails to adequately sanitize user-controlled input, allowing an attacker to inject arbitrary JavaScript expressions that are then executed by the sandbox [2].
Exploitation
An attacker can exploit this vulnerability by providing malicious input to any application that passes user-controlled text to expressions.compile(). No authentication or special network position is required beyond the ability to supply input to the vulnerable function [1][3]. The exploitation surface exists both client-side (in browser contexts) and server-side (Node.js environments) [3].
Impact
Successful exploitation leads to arbitrary JavaScript execution. In a browser, this enables the attacker to run any script in the context of the user's session (e.g., stealing cookies or performing actions on behalf of the user). On the server, it results in full remote code execution with the privileges of the Node.js process [1][3].
Mitigation
The maintainers patched the issue in version 1.0.1 by restricting access to the prototype chain [2]. Users should upgrade immediately. As a workaround, applications can block all user-controlled input to expressions.compile(), or restrict allowed characters to a safe regex pattern (letters, digits, spaces, and a limited set of symbols) [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.0.1 | 1.0.1 |
Affected products
2- Range: < 1.0.1
Patches
1061addfb9a9eDisallow access to prototype chain (CVE-2020-5219)
10 files changed · +11136 −3232
.editorconfig+13 −0 added@@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.js] +charset = utf-8 +indent_style = tab + +[{.travis.yml,package.json}] +indent_style = space +indent_size = 2
.eslintignore+0 −1 modified@@ -1,2 +1 @@ node_modules -lib/parse.js \ No newline at end of file
.eslintrc+185 −6 modified@@ -1,8 +1,187 @@ { - "extends": "peerigon/es5", - "root": true, - "env": { - "node": true, - "browser": true - } + "parserOptions" : { + "ecmaVersion": 7 + }, + "plugins": [], + "globals":{ + "Promise": true, + "JQLite": true, + "JQLitePrototype": true, + "ArrayBuffer": true + }, + "env": { + "node": true, + "browser": true, + "mocha": true + }, + "rules": { + "accessor-pairs": 2, + "array-bracket-spacing": [2, "never"], + "arrow-parens": 0, + "arrow-spacing": [2, {"before": true, "after": true}], + "block-scoped-var": 2, + "block-spacing": [2, "always"], + "brace-style": 0, + "callback-return": 2, + "camelcase": [0 , {"properties": "never"}], + "comma-dangle": [0 , "always-multiline"], + "comma-spacing": [2, {"before": false, "after": true}], + "comma-style": [2, "last"], + "complexity": [2, 10], + "computed-property-spacing": [2, "never"], + "consistent-return": 0, + "consistent-this": [2, "self"], + "constructor-super": 2, + "curly": [2, "all"], + "default-case": 0, + "dot-location": [2, "property"], + "dot-notation": 2, + "eol-last": 2, + "eqeqeq": [2, "smart"], + "func-names": 0, + "func-style": [2, "declaration"], + "generator-star-spacing": [2, {"before": false, "after": true}], + "global-require": 0, + "guard-for-in": 2, + "handle-callback-err": 2, + "id-length": 0, + "id-match": 0, + "init-declarations": 0, + "key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "strict"}], + "linebreak-style": 0, + "lines-around-comment": 0, + "max-nested-callbacks": 0, + "new-cap": [2, { + "newIsCapExceptions": ["Boom.badRequest", "Boom.forbidden", "Boom.unauthorized", "Boom.wrap"], + "capIsNewExceptions": ["squeeze.Squeeze"] + }], + "new-parens": 2, + "newline-after-var": 0, + "no-alert": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 0, + "no-class-assign": 2, + "no-console": 2, + "no-const-assign": 2, + "no-constant-condition": [2, {"checkLoops": false}], + "no-continue": 0, + "no-control-regex": 0, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 2, + "no-empty": 2, + "no-empty-character-class": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-extra-semi": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implicit-coercion": 0, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 0, + "no-lone-blocks": 2, + "no-lonely-if": 2, + "no-loop-func": 2, + "no-mixed-requires": 2, + "no-mixed-spaces-and-tabs": 0, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, {"max": 1}], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 0, + "no-path-concat": 2, + "no-process-env": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-modules": 0, + "no-restricted-syntax": 0, + "no-return-assign": 0, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-ternary": 0, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 0, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unexpected-multiline": 2, + "no-unneeded-ternary": 2, + "no-unreachable": 2, + "no-unused-expressions": 2, + "no-unused-vars": 2, + "no-use-before-define": [2, "nofunc"], + "no-useless-call": 2, + "no-useless-concat": 2, + "no-var": 0, + "no-void": 2, + "no-warning-comments": [2, {"terms": ["todo", "fixme"], "location": "anywhere"}], + "no-with": 2, + "object-curly-spacing": 0, + "object-shorthand": 0, + "one-var": 0, + "operator-assignment": [0, "always"], + "operator-linebreak": 0, + "padded-blocks": [2, "never"], + "prefer-arrow-callback": 0, + "prefer-const": 0, + "prefer-destructuring": [0, { + "AssignmentExpression": { + "array": true, + "object": false + } + }], + "prefer-reflect": 0, + "prefer-spread": 0, + "prefer-template": 0, + "quotes": [2, "double", {"avoidEscape":true}], + "quote-props": [2, "as-needed"], + "radix": 2, + "require-jsdoc": 0, + "require-yield": 2, + "semi": [2, "always"], + "semi-spacing": [2, {"before": false, "after": true}], + "sort-vars": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": [2, "inside"], + "wrap-regex": 0, + "yoda": [2, "never"] + } }
lib/main.js+35 −28 modified@@ -6,24 +6,29 @@ var filters = {}; var Lexer = parse.Lexer; var Parser = parse.Parser; var parserOptions = { - csp: false, // noUnsafeEval, - expensiveChecks: true, - literals: { // defined at: function $ParseProvider() { - true: true, - false: false, - null: null, - /*eslint no-undefined: 0*/ - undefined: undefined - /* eslint: no-undefined: 1 */ - } - //isIdentifierStart: undefined, //isFunction(identStart) && identStart, - //isIdentifierContinue: undefined //isFunction(identContinue) && identContinue + csp: false, // noUnsafeEval, + expensiveChecks: true, + literals: { + // defined at: function $ParseProvider() { + true: true, + false: false, + null: null, + /*eslint no-undefined: 0*/ + undefined: undefined + /* eslint: no-undefined: 1 */ + } + //isIdentifierStart: undefined, //isFunction(identStart) && identStart, + //isIdentifierContinue: undefined //isFunction(identContinue) && identContinue }; var lexer = new Lexer({}); -var parser = new Parser(lexer, function getFilter(name) { - return filters[name]; -}, parserOptions); +var parser = new Parser( + lexer, + function getFilter(name) { + return filters[name]; + }, + parserOptions +); /** * Compiles src and returns a function that executes src on a target object. @@ -33,22 +38,24 @@ var parser = new Parser(lexer, function getFilter(name) { * @returns {function} */ function compile(src) { - var cached; + var cached; - if (typeof src !== "string") { - throw new TypeError("src must be a string, instead saw '" + typeof src + "'"); - } + if (typeof src !== "string") { + throw new TypeError( + "src must be a string, instead saw '" + typeof src + "'" + ); + } - if (!compile.cache) { - return parser.parse(src); - } + if (!compile.cache) { + return parser.parse(src); + } - cached = compile.cache[src]; - if (!cached) { - cached = compile.cache[src] = parser.parse(src); - } + cached = compile.cache[src]; + if (!cached) { + cached = compile.cache[src] = parser.parse(src); + } - return cached; + return cached; } /** @@ -57,7 +64,7 @@ function compile(src) { * * @type {object} */ -compile.cache = {}; +compile.cache = Object.create(null); exports.Lexer = Lexer; exports.Parser = Parser;
lib/parse.js+3234 −2704 modifiedpackage.json+9 −7 modified@@ -5,8 +5,8 @@ "main": "./lib/main.js", "scripts": { "test": "mocha test/main.test.js -R spec", - "test-browser": "webpack-dev-server mocha\\!./test/main.test.js -d", - "posttest": "eslint --fix lib test" + "test-browser": "webpack-dev-server 'mocha-loader!./test/main.test.js' --output-filename output.js --port 8081", + "posttest": "eslint --fix lib test && prettier --write lib/*.js test/*.js" }, "keywords": [ "angular", @@ -20,11 +20,13 @@ "dependencies": {}, "devDependencies": { "chai": "^3.2.0", - "eslint": "^1.5.0", - "eslint-config-peerigon": "^0.1.0", - "mocha": "^2.3.2", - "mocha-loader": "^0.7.1", - "webpack-dev-server": "^1.11.0" + "eslint": "^5.16.0", + "mocha": "^7.0.0", + "mocha-loader": "^4.0.1", + "prettier": "^1.19.1", + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10", + "webpack-dev-server": "^3.10.1" }, "repository": { "type": "git",
package-lock.json+7178 −0 addedREADME.md+27 −35 modified@@ -1,5 +1,6 @@ -angular-expressions -=================== +**A security vulnerability has been found that affects all versions before 1.0.1. Please read this advisory https://github.com/peerigon/angular-expressions/security/advisories/GHSA-hxhm-96pp-2m43 for more information.** + +# angular-expressions **[angular's nicest part](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19688bb9bbe504c300e61776dc/src/ng/parse.js) extracted as a standalone module for the browser and node.** @@ -38,16 +39,13 @@ Check out [their readme](http://docs.angularjs.org/guide/expression) for further <br /> -Setup ------ +## Setup [](https://npmjs.org/package/angular-expressions) - <br /> -Filters -------- +## Filters Angular provides a mechanism to define filters on expressions: @@ -62,25 +60,24 @@ Arguments are evaluated against the scope: ```javascript expressions.filters.currency = (input, currency, digits) => { - input = input.toFixed(digits); + input = input.toFixed(digits); - if (currency === "EUR") { - return input + "€"; - } else { - return input + "$"; - } + if (currency === "EUR") { + return input + "€"; + } else { + return input + "$"; + } }; expr = expressions.compile("1.2345 | currency:selectedCurrency:2"); expr({ - selectedCurrency: "EUR" + selectedCurrency: "EUR" }); // returns '1.23€' ``` <br /> -API ----- +## API ### exports @@ -104,9 +101,9 @@ Example output of: `compile("tmp + 1").ast` constant: false } ``` -*NOTE* angular $parse do not export ast variable it's done by this library. +_NOTE_ angular \$parse do not export ast variable it's done by this library. -#### .compile.cache = {} +#### .compile.cache = Object.create(null) A cache containing all compiled functions. The src is used as key. Set this on `false` to disable the cache. @@ -122,34 +119,34 @@ The internal [Lexer](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19 The internal [Parser](https://github.com/angular/angular.js/blob/6b049c74ccc9ee19688bb9bbe504c300e61776dc/src/ng/parse.js#L390). ----- +--- -### evaluate(scope?): * +### evaluate(scope?): \* Evaluates the compiled `src` and returns the result of the expression. Property look-ups or assignments are executed on a given `scope`. -### evaluate.assign(scope, value): * +### evaluate.assign(scope, value): \* Tries to assign the given `value` to the result of the compiled expression on the given `scope` and returns the result of the assignment. <br /> -In the browser --------------- +## In the browser There is no `dist` build because it's not 2005 anymore. Use a module bundler like [webpack](http://webpack.github.io/) or [browserify](http://browserify.org/). They're both capable of CommonJS and AMD. <br /> -Security --------- +## Security + +The code of angular was not secured from reading prototype, and since version 1.0.1 of angular-expressions, the module disallows reading properties that are not ownProperties. See [this blog post](http://blog.angularjs.org/2016/09/angular-16-expression-sandbox-removal.html) for more details about the sandbox that got removed completely in angular 1.6. Comment from `angular.js/src/ng/parse.js`: --- Angular expressions are generally considered safe because these expressions only have direct -access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by +access to \$scope and locals. However, one can obtain the ability to execute arbitrary JS code by obtaining a reference to native JS functions such as the Function constructor. As an example, consider the following Angular expression: @@ -180,16 +177,13 @@ window or some DOM object that has a reference to window is published onto a Sco <br /> +## Authorship -Authorship ----------- Kudos go entirely to the great angular.js team, it's their implementation! - <br /> -Contributing ------------- +## Contributing Suggestions and bug-fixes are always appreciated. Don't hesitate to create an issue or pull-request. All contributed code should pass @@ -198,12 +192,10 @@ Suggestions and bug-fixes are always appreciated. Don't hesitate to create an is <br /> -License -------- +## License [Unlicense](http://unlicense.org/) -Sponsors -------- +## Sponsors [<img src="https://assets.peerigon.com/peerigon/logo/peerigon-logo-flat-spinat.png" width="150" />](https://peerigon.com)
test/.eslintrc+0 −3 removed@@ -1,3 +0,0 @@ -{ - "extends": "peerigon/es5tests" -}
test/main.test.js+455 −448 modified@@ -9,466 +9,473 @@ chai.config.includeStack = true; // These tests make no claim to be complete. We only test the most important parts of angular expressions. // I hope they have their own tests ;) -describe("expressions", function () { - - describe(".Lexer", function () { - - it("should be a function", function () { - expect(expressions.Lexer).to.be.a("function"); - }); - - it("should provide a .lex()-method", function () { - var lexer = new expressions.Lexer(); - - expect(lexer.lex).to.be.a("function"); - }); - - }); - - describe(".Parser", function () { - - it("should be a function", function () { - expect(expressions.Parser).to.be.a("function"); - }); - - it("should provide a .parse()-method", function () { - var parser = new expressions.Parser(undefined, undefined, {}); - - expect(parser.parse).to.be.a("function"); - }); - - }); - - describe(".compile(src)", function () { - var scope; - var evaluate; - - beforeEach(function () { - scope = { - ship: { - pirate: { - name: "Jenny" - } - } - }; - }); - - it("should return a function", function () { - expect(compile("")).to.be.a("function"); - }); - - it("should throw an error if the given value is not a string", function () { - expect(function () { - compile(); - }).to.throw("src must be a string, instead saw 'undefined'"); - }); - - it("should expose the ast", function () { - expect(compile("tmp").ast).to.be.a("object"); - }); - - describe("when evaluating literals", function () { - - it("should return null", function () { - evaluate = compile("null"); - expect(evaluate(scope)).to.equal(null); - }); - - it("should return true", function () { - evaluate = compile("true"); - expect(evaluate(scope)).to.equal(true); - }); - - it("should return false", function () { - evaluate = compile("false"); - expect(evaluate(scope)).to.equal(false); - }); - - it("should return 2.34e5", function () { - evaluate = compile("2.34e5"); - expect(evaluate(scope)).to.equal(2.34e5); - }); - - it("should return 'string'", function () { - evaluate = compile("'string'"); - expect(evaluate(scope)).to.equal("string"); - }); - - it("should return [ship, 1, 2, []]", function () { - evaluate = compile("[ship, 1, 2, []]"); - expect(evaluate(scope)).to.eql([scope.ship, 1, 2, []]); - }); - - it("should return { test: 'value', 'new-object': {} }", function () { - evaluate = compile("{ test: 'value', 'new-object': {} }"); - expect(evaluate(scope)).to.eql({ test: "value", "new-object": {} }); - }); - - }); - - describe("when evaluating simple key look-ups", function () { - - it("should return the value if its defined on scope", function () { - evaluate = compile("ship"); - expect(evaluate(scope)).to.equal(scope.ship); - }); - - it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { - evaluate = compile("notDefined"); - expect(evaluate(scope)).to.equal(undefined); - }); - - it("should return the scope even when the 'this' keyword is used", function () { - evaluate = compile("this"); - expect(evaluate(scope)).to.equal(scope); - }); - - }); - - describe("when evaluating simple assignments", function () { - - it("should set the new value on scope", function () { - evaluate = compile("newValue = 'new'"); - evaluate(scope); - expect(scope.newValue).to.equal("new"); - }); - - it("should change the value if its defined on scope", function () { - evaluate = compile("ship = 'ship'"); - evaluate(scope); - expect(scope.ship).to.equal("ship"); - }); - - }); - - describe("when evaluating dot-notated loop-ups", function () { - - it("should return the value if its defined on scope", function () { - evaluate = compile("ship.pirate.name"); - expect(evaluate(scope)).to.equal("Jenny"); - }); - - it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { - evaluate = compile("island.pirate.name"); - expect(evaluate(scope)).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"); - }); - - it("should change the value if its defined on scope", function () { - evaluate = compile("ship.pirate.name = 'Störtebeker'"); - evaluate(scope); - expect(scope.ship.pirate.name).to.equal("Störtebeker"); - }); - - }); - - describe("when evaluating array look-ups", function () { - - beforeEach(function () { - scope.ships = [ - { pirate: "Jenny" }, - { pirate: "Störtebeker" } - ]; - }); - - it("should return the value if its defined on scope", function () { - evaluate = compile("ships[1].pirate"); - expect(evaluate(scope)).to.equal("Störtebeker"); - }); - - it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function () { - evaluate = compile("ships[2].pirate"); - expect(evaluate(scope)).to.equal(undefined); - }); - - }); - - describe("when evaluating array assignments", function () { - - it("should change the value if its defined on scope", function () { - scope.ships = [ - { pirate: "Jenny" } - ]; - evaluate = compile("ships[0].pirate = 'Störtebeker'"); - evaluate(scope); - expect(scope.ships[0].pirate).to.equal("Störtebeker"); - }); - - }); - - describe("when evaluating function calls", function () { - - describe("using no arguments", function () { - - it("should return the function's return value", function () { - scope.findPirate = function () { - return scope.ship.pirate; - }; - - evaluate = compile("findPirate()"); - expect(evaluate(scope)).to.equal(scope.ship.pirate); - }); - - it("should call the function on the scope", function () { - scope.returnThis = function () { - return this; - }; - evaluate = compile("returnThis()"); - expect(evaluate(scope)).to.equal(scope); - }); - - it("should call the function on the object where it is defined", function () { - scope.ship.returnThis = function () { - return this; - }; - evaluate = compile("ship.returnThis()"); - expect(evaluate(scope)).to.equal(scope.ship); - }); - - }); - - describe("using arguments", function () { - - it("should parse the arguments accordingly", function () { - scope.findPirate = function (pirate) { - return Array.prototype.slice.call(arguments); - }; - evaluate = compile("findPirate(ship.pirate, 1, [2, 3])"); - expect(evaluate(scope)).to.eql([scope.ship.pirate, 1, [2, 3]]); - }); - - }); - - }); - - describe("when evaluating operators", function () { - - it("should return the expected result when using +", function () { - evaluate = compile("1 + 1"); - expect(evaluate()).to.equal(2); - }); - - it("should return the expected result when using -", function () { - evaluate = compile("1 - 1"); - expect(evaluate()).to.equal(0); - }); - - it("should return the expected result when using *", function () { - evaluate = compile("2 * 2"); - expect(evaluate()).to.equal(4); - }); - - it("should return the expected result when using /", function () { - evaluate = compile("4 / 2"); - expect(evaluate()).to.equal(2); - }); - - it("should return the expected result when using %", function () { - evaluate = compile("3 % 2"); - expect(evaluate()).to.equal(1); - }); - - it("should return the expected result when using &&", function () { - evaluate = compile("true && true"); - expect(evaluate()).to.equal(true); - evaluate = compile("true && false"); - expect(evaluate()).to.equal(false); - evaluate = compile("false && false"); - expect(evaluate()).to.equal(false); - }); - - it("should return the expected result when using ||", function () { - evaluate = compile("true || true"); - expect(evaluate()).to.equal(true); - evaluate = compile("true || false"); - expect(evaluate()).to.equal(true); - evaluate = compile("false || false"); - expect(evaluate()).to.equal(false); - }); - - it("should return the expected result when using !", function () { - evaluate = compile("!true"); - expect(evaluate()).to.equal(false); - evaluate = compile("!false"); - expect(evaluate()).to.equal(true); - }); - - /* Ooops, angular doesn't support ++. Maybe someday? +describe("expressions", function() { + describe(".Lexer", function() { + it("should be a function", function() { + expect(expressions.Lexer).to.be.a("function"); + }); + + it("should provide a .lex()-method", function() { + var lexer = new expressions.Lexer(); + + expect(lexer.lex).to.be.a("function"); + }); + }); + + describe(".Parser", function() { + it("should be a function", function() { + expect(expressions.Parser).to.be.a("function"); + }); + + it("should provide a .parse()-method", function() { + var parser = new expressions.Parser(undefined, undefined, {}); + + expect(parser.parse).to.be.a("function"); + }); + }); + + describe(".compile(src)", function() { + var scope; + var evaluate; + + beforeEach(function() { + scope = { + ship: { + pirate: { + name: "Jenny" + } + } + }; + }); + + it("should return a function", function() { + expect(compile("")).to.be.a("function"); + }); + + it("should throw an error if the given value is not a string", function() { + expect(function() { + compile(); + }).to.throw("src must be a string, instead saw 'undefined'"); + }); + + it("should expose the ast", function() { + expect(compile("tmp").ast).to.be.a("object"); + }); + + describe("when evaluating literals", function() { + it("should return null", function() { + evaluate = compile("null"); + expect(evaluate(scope)).to.equal(null); + }); + + it("should return true", function() { + evaluate = compile("true"); + expect(evaluate(scope)).to.equal(true); + }); + + it("should return false", function() { + evaluate = compile("false"); + expect(evaluate(scope)).to.equal(false); + }); + + it("should return 2.34e5", function() { + evaluate = compile("2.34e5"); + expect(evaluate(scope)).to.equal(2.34e5); + }); + + it("should return 'string'", function() { + evaluate = compile("'string'"); + expect(evaluate(scope)).to.equal("string"); + }); + + it("should return [ship, 1, 2, []]", function() { + evaluate = compile("[ship, 1, 2, []]"); + expect(evaluate(scope)).to.eql([scope.ship, 1, 2, []]); + }); + + it("should return { test: 'value', 'new-object': {} }", function() { + evaluate = compile("{ test: 'value', 'new-object': {} }"); + expect(evaluate(scope)).to.eql({ test: "value", "new-object": {} }); + }); + + it("should return context value when nothing in the scope", function() { + evaluate = compile("test"); + expect(evaluate(scope, { test: "hello" })).to.equal("hello"); + }); + + it("should return context value when something in the scope", function() { + evaluate = compile("test"); + expect(evaluate({ test: "bye" }, { test: "hello" })).to.equal("hello"); + }); + }); + + describe("when evaluating simple key look-ups", function() { + it("should return the value if its defined on scope", function() { + evaluate = compile("ship"); + expect(evaluate(scope)).to.equal(scope.ship); + }); + + it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function() { + evaluate = compile("notDefined"); + expect(evaluate(scope)).to.equal(undefined); + }); + + it("should return the scope even when the 'this' keyword is used", function() { + evaluate = compile("this"); + expect(evaluate(scope)).to.equal(scope); + }); + }); + + describe("when evaluating simple assignments", function() { + it("should set the new value on scope", function() { + evaluate = compile("newValue = 'new'"); + evaluate(scope); + expect(scope.newValue).to.equal("new"); + }); + + it("should change the value if its defined on scope", function() { + evaluate = compile("ship = 'ship'"); + evaluate(scope); + expect(scope.ship).to.equal("ship"); + }); + }); + + describe("when evaluating dot-notated loop-ups", function() { + it("should return the value if its defined on scope", function() { + evaluate = compile("ship.pirate.name"); + expect(evaluate(scope)).to.equal("Jenny"); + }); + + it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function() { + evaluate = compile("island.pirate.name"); + expect(evaluate(scope)).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"); + }); + + it("should change the value if its defined on scope", function() { + evaluate = compile("ship.pirate.name = 'Störtebeker'"); + evaluate(scope); + expect(scope.ship.pirate.name).to.equal("Störtebeker"); + }); + }); + + describe("when evaluating array look-ups", function() { + beforeEach(function() { + scope.ships = [{ pirate: "Jenny" }, { pirate: "Störtebeker" }]; + }); + + it("should return the value if its defined on scope", function() { + evaluate = compile("ships[1].pirate"); + expect(evaluate(scope)).to.equal("Störtebeker"); + }); + + it("should return undefined instead of throwing a ReferenceError if it's not defined on scope", function() { + evaluate = compile("ships[2].pirate"); + expect(evaluate(scope)).to.equal(undefined); + }); + }); + + describe("when evaluating array assignments", function() { + it("should change the value if its defined on scope", function() { + scope.ships = [{ pirate: "Jenny" }]; + evaluate = compile("ships[0].pirate = 'Störtebeker'"); + evaluate(scope); + expect(scope.ships[0].pirate).to.equal("Störtebeker"); + }); + }); + + describe("when evaluating function calls", function() { + describe("using no arguments", function() { + it("should return the function's return value", function() { + scope.findPirate = function() { + return scope.ship.pirate; + }; + + evaluate = compile("findPirate()"); + expect(evaluate(scope)).to.equal(scope.ship.pirate); + }); + + it("should call the function on the scope", function() { + scope.returnThis = function() { + return this; + }; + evaluate = compile("returnThis()"); + expect(evaluate(scope)).to.equal(scope); + }); + + it("should call the function on the object where it is defined", function() { + scope.ship.returnThis = function() { + return this; + }; + evaluate = compile("ship.returnThis()"); + expect(evaluate(scope)).to.equal(scope.ship); + }); + }); + + describe("using arguments", function() { + it("should parse the arguments accordingly", function() { + scope.findPirate = function() { + return Array.prototype.slice.call(arguments); + }; + evaluate = compile("findPirate(ship.pirate, 1, [2, 3])"); + expect(evaluate(scope)).to.eql([scope.ship.pirate, 1, [2, 3]]); + }); + }); + }); + + describe("when evaluating operators", function() { + it("should return the expected result when using +", function() { + evaluate = compile("1 + 1"); + expect(evaluate()).to.equal(2); + }); + + it("should return the expected result when using -", function() { + evaluate = compile("1 - 1"); + expect(evaluate()).to.equal(0); + }); + + it("should return the expected result when using *", function() { + evaluate = compile("2 * 2"); + expect(evaluate()).to.equal(4); + }); + + it("should return the expected result when using /", function() { + evaluate = compile("4 / 2"); + expect(evaluate()).to.equal(2); + }); + + it("should return the expected result when using %", function() { + evaluate = compile("3 % 2"); + expect(evaluate()).to.equal(1); + }); + + it("should return the expected result when using &&", function() { + evaluate = compile("true && true"); + expect(evaluate()).to.equal(true); + evaluate = compile("true && false"); + expect(evaluate()).to.equal(false); + evaluate = compile("false && false"); + expect(evaluate()).to.equal(false); + }); + + it("should return the expected result when using ||", function() { + evaluate = compile("true || true"); + expect(evaluate()).to.equal(true); + evaluate = compile("true || false"); + expect(evaluate()).to.equal(true); + evaluate = compile("false || false"); + expect(evaluate()).to.equal(false); + }); + + it("should return the expected result when using !", function() { + evaluate = compile("!true"); + expect(evaluate()).to.equal(false); + evaluate = compile("!false"); + expect(evaluate()).to.equal(true); + }); + + /* Ooops, angular doesn't support ++. Maybe someday? it("should return the expected result when using ++", function () { scope.value = 2; evaluate = compile("value++"); expect(evaluate()).to.equal(3); expect(scope.value).to.equal(3); });*/ - /* Ooops, angular doesn't support --. Maybe someday? + /* Ooops, angular doesn't support --. Maybe someday? it("should return the expected result when using --", function () { scope.value = 2; evaluate = compile("value--"); expect(evaluate()).to.equal(1); expect(scope.value).to.equal(1); });*/ - it("should return the expected result when using ?", function () { - evaluate = compile("true? 'it works' : false"); - expect(evaluate()).to.equal("it works"); - evaluate = compile("false? false : 'it works'"); - expect(evaluate()).to.equal("it works"); - }); - - }); - - describe("using complex expressions", function () { - - beforeEach(function () { - scope.ships = [ - { pirate: function (str) { return str; } }, - { pirate: function (str) { return str; } } - ]; - scope.index = 0; - scope.pi = "pi"; - scope.Jenny = "Jenny"; - }); - - it("should still be parseable and executable", function () { - evaluate = compile("ships[index][pi + 'rate'](Jenny)"); - expect(evaluate(scope)).to.equal("Jenny"); - }); - - }); - - describe("when evaluating syntactical errors", function () { - - it("should give a readable error message", function () { - expect(function () { - compile("'unterminated string"); - }).to.throw("Lexer Error: Unterminated quote at columns 0-20 ['unterminated string] in expression ['unterminated string]."); - }); - - it("should give a readable error message", function () { - expect(function () { - compile("3 = 4"); - }).to.throw("[$parse:lval] Trying to assign a value to a non l-value\nhttp://errors.angularjs.org/\"NG_VERSION_FULL\"/$parse/lval"); - }); - - }); - - describe("when using filters", function () { - - it("should apply the given filter", function () { - expressions.filters.currency = function (input, currency, digits) { - input = input.toFixed(digits); - - if (currency === "EUR") { - return input + "€"; - } - return input + "$"; - }; - - evaluate = compile("1.2345 | currency:selectedCurrency:2"); - expect(evaluate({ - selectedCurrency: "EUR" - })).to.equal("1.23€"); - }); - - }); - - describe("when evaluating the same expression multiple times", function () { - - it("should cache the generated function", function () { - expect(compile("a")).to.equal(compile("a")); - }); - - }); - - describe("for assigning values", function () { - - beforeEach(function () { - scope = {}; - }); - - it("should expose an 'assign'-function", function () { - var fn = compile("a"); - - expect(fn.assign).to.be.a("function"); - fn.assign(scope, 123); - expect(scope.a).to.equal(123); - }); - - describe("the 'assign'-function", function () { - - it("should work for expressions ending with brackets", function () { - var fn = compile("a.b['c']"); - - fn.assign(scope, 123); - expect(scope.a.b.c).to.equal(123); - }); - - it("should work for expressions with brackets in the middle", function () { - var fn = compile("a[\"b\"].c"); - - fn.assign(scope, 123); - expect(scope.a.b.c).to.equal(123); - }); - - it("should work for expressions with brackets in the middle", function () { - var fn = compile("a[\"b\"].c"); - - fn.assign(scope, 123); - expect(scope.a.b.c).to.equal(123); - }); - - it("should return the result of the assignment", function () { - var fn = compile("a[\"b\"].c"); - - expect(fn.assign(scope, 123)).to.equal(123); - }); - - }); - - }); - - describe(".cache", function () { - - it("should be an object by default", function () { - expect(compile.cache).to.be.an("object"); - }); - - it("should cache the generated function by the expression", function () { - var fn = compile("a"); - - expect(compile.cache.a).to.equal(fn); - }); - - describe("when setting it to false", function () { - - it("should disable the cache", function () { - compile.cache = false; - expect(compile("a")).to.not.equal(compile("a")); - compile.cache = {}; - }); - - }); - - }); - - }); - - describe(".filters", function () { - - it("should be an object", function () { - expect(expressions.filters).to.be.an("object"); - }); - - }); - + it("should return the expected result when using ?", function() { + evaluate = compile("true? 'it works' : false"); + expect(evaluate()).to.equal("it works"); + evaluate = compile("false? false : 'it works'"); + expect(evaluate()).to.equal("it works"); + }); + }); + + describe("using complex expressions", function() { + beforeEach(function() { + scope.ships = [ + { + pirate: function(str) { + return str; + } + }, + { + pirate: function(str) { + return str; + } + } + ]; + scope.index = 0; + scope.pi = "pi"; + scope.Jenny = "Jenny"; + }); + + it("should still be parseable and executable", function() { + evaluate = compile("ships[index][pi + 'rate'](Jenny)"); + expect(evaluate(scope)).to.equal("Jenny"); + }); + }); + + describe("when evaluating syntactical errors", function() { + it("should give a readable error message", function() { + expect(function() { + compile("'unterminated string"); + }).to.throw( + "Lexer Error: Unterminated quote at columns 0-20 ['unterminated string] in expression ['unterminated string]." + ); + }); + + it("should give a readable error message", function() { + expect(function() { + compile("3 = 4"); + }).to.throw( + '[$parse:lval] Trying to assign a value to a non l-value\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/$parse/lval' + ); + }); + }); + + describe("when using filters", function() { + it("should apply the given filter", function() { + expressions.filters.currency = function(input, currency, digits) { + input = input.toFixed(digits); + + if (currency === "EUR") { + return input + "€"; + } + return input + "$"; + }; + + evaluate = compile("1.2345 | currency:selectedCurrency:2"); + expect( + evaluate({ + selectedCurrency: "EUR" + }) + ).to.equal("1.23€"); + }); + }); + + describe("when evaluating the same expression multiple times", function() { + it("should cache the generated function", function() { + expect(compile("a")).to.equal(compile("a")); + }); + }); + + describe("for assigning values", function() { + beforeEach(function() { + scope = {}; + }); + + it("should expose an 'assign'-function", function() { + var fn = compile("a"); + + expect(fn.assign).to.be.a("function"); + fn.assign(scope, 123); + expect(scope.a).to.equal(123); + }); + + describe("the 'assign'-function", function() { + it("should work for expressions ending with brackets", function() { + var fn = compile("a.b['c']"); + + fn.assign(scope, 123); + expect(scope.a.b.c).to.equal(123); + }); + + it("should work for expressions with brackets in the middle", function() { + var fn = compile('a["b"].c'); + + fn.assign(scope, 123); + expect(scope.a.b.c).to.equal(123); + }); + + it("should return the result of the assignment", function() { + var fn = compile('a["b"].c'); + + expect(fn.assign(scope, 123)).to.equal(123); + }); + }); + }); + + describe(".cache", function() { + it("should be an object by default", function() { + expect(compile.cache).to.be.an("object"); + }); + + it("should cache the generated function by the expression", function() { + var fn = compile("a"); + + expect(compile.cache.a).to.equal(fn); + }); + + describe("when setting it to false", function() { + it("should disable the cache", function() { + compile.cache = false; + expect(compile("a")).to.not.equal(compile("a")); + compile.cache = Object.create(null); + }); + }); + }); + }); + + describe(".filters", function() { + it("should be an object", function() { + expect(expressions.filters).to.be.an("object"); + }); + }); + + describe("prototype", function() { + var evaluate; + + it("should not leak", function() { + evaluate = compile("''.split"); + expect(evaluate({})).to.eql(undefined); + }); + + it("should not leak with computed prop", function() { + evaluate = compile("a['split']"); + expect(evaluate({ a: "" })).to.eql(undefined); + }); + + it("should allow to read string length", function() { + evaluate = compile("'abc'.length"); + expect(evaluate({})).to.eql(3); + }); + + it("should allow to read users length", function() { + evaluate = compile("users.length"); + expect(evaluate({ users: [1, 4, 4] })).to.eql(3); + }); + + // it("should disallow from changing prototype", 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); + }); + + it("should work with toString", function() { + evaluate = compile("toString"); + expect(evaluate({ toString: 10 })).to.eql(10); + }); + }); });
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-hxhm-96pp-2m43ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-5219ghsaADVISORY
- blog.angularjs.org/2016/09/angular-16-expression-sandbox-removal.htmlghsax_refsource_MISCWEB
- github.com/peerigon/angular-expressions/commit/061addfb9a9e932a970e5fcb913d020038e65667ghsax_refsource_MISCWEB
- github.com/peerigon/angular-expressions/security/advisories/GHSA-hxhm-96pp-2m43ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.