From c207bff1f238d06e56a4c12a90f34ec77cf2524b Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 25 Jul 2016 21:14:29 +0300 Subject: [PATCH 1/3] test($parse): test more constructors against `isecaf` error --- test/ng/parseSpec.js | 122 +++++++++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index 116bd9f06de2..ab2f3d6d1f9c 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2958,38 +2958,106 @@ describe('parser', function() { }); it('should prevent assigning in the context of a constructor', function() { - expect(function() { - scope.$eval("''.constructor.join"); - }).not.toThrow(); - expect(function() { - scope.$eval("''.constructor.join = ''.constructor.join"); - }).toThrow(); - expect(function() { - scope.$eval("''.constructor[0] = ''"); - }).toThrow(); - expect(function() { - scope.$eval("(0).constructor[0] = ''"); - }).toThrow(); - expect(function() { - scope.$eval("{}.constructor[0] = ''"); - }).toThrow(); - // foo.constructor is the object constructor. - expect(function() { - scope.$eval("foo.constructor[0] = ''", {foo: {}}); - }).toThrow(); + forEach({ + '(true)': true, + '(1)': 1, + '"string"': 'string', + '[]': [] + }, function(thing, expr) { + var constructorExpr = expr + '.constructor'; + + expect(function() { + scope.$eval(constructorExpr + '.join'); + }).not.toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '.join'); + }).not.toThrow(); + expect(function() { + scope.$eval(constructorExpr + '.join = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(constructorExpr + '[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + + expect(function() { + scope.foo = thing; + scope.$eval('foo.constructor[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: thing}); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + + // These might throw different error (e.g. isecobj, isecfn), + // but still having them here for good measure + forEach({ + '{}': {}, + '$eval': scope.$eval + }, function(thing, expr) { + var constructorExpr = expr + '.constructor'; + + expect(function() { + scope.$eval(constructorExpr + '.join'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '.join'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(constructorExpr + '.join = ""'); + }).toThrow(); + expect(function() { + scope.$eval(constructorExpr + '[0] = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); + }).toThrow(); + + expect(function() { + scope.foo = thing; + scope.$eval('foo.constructor[0] = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: thing}); + }).toThrow(); + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + // foo.constructor is not a constructor. expect(function() { - scope.$eval("foo.constructor[0] = ''", {foo: {constructor: ''}}); + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: {constructor: ''}}); }).not.toThrow(); + expect(function() { - scope.$eval("objConstructor = {}.constructor; objConstructor.join = ''"); - }).toThrow(); - expect(function() { - scope.$eval("'a'.constructor.prototype.charAt=[].join"); - }).toThrow(); + scope.$eval('"a".constructor.prototype.charAt = [].join'); + }).toThrowMinErr('$parse', 'isecaf'); expect(function() { - scope.$eval("'a'.constructor.prototype.charCodeAt=[].concat"); - }).toThrow(); + scope.$eval('"a".constructor.prototype.charCodeAt = [].concat'); + }).toThrowMinErr('$parse', 'isecaf'); }); }); From a50cea28831cffe5b0f0ab68773dd3cfbce51b49 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 25 Jul 2016 22:02:20 +0300 Subject: [PATCH 2/3] fix($parse): block assigning to fields of a constructor prototype Fixes #14939 --- src/ng/parse.js | 24 +++- test/ng/parseSpec.js | 276 ++++++++++++++++++++++++++++++------------- 2 files changed, 216 insertions(+), 84 deletions(-) diff --git a/src/ng/parse.js b/src/ng/parse.js index 3599b7f25947..e95e69f71ce6 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -113,10 +113,28 @@ function ensureSafeFunction(obj, fullExpression) { function ensureSafeAssignContext(obj, fullExpression) { if (obj) { - if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor || - obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { + var booleanConstructor = (false).constructor; + var numberConstructor = (0).constructor; + var stringConstructor = ''.constructor; + var objectConstructor = {}.constructor; + var arrayConstructor = [].constructor; + var functionConstructor = Function.constructor; + + if (obj === booleanConstructor || + obj === numberConstructor || + obj === stringConstructor || + obj === objectConstructor || + obj === arrayConstructor || + obj === functionConstructor || + obj === booleanConstructor.prototype || + obj === numberConstructor.prototype || + obj === stringConstructor.prototype || + obj === objectConstructor.prototype || + obj === arrayConstructor.prototype || + obj === functionConstructor.prototype) { throw $parseMinErr('isecaf', - 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression); + 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', + fullExpression); } } } diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index ab2f3d6d1f9c..230defc30a17 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2957,95 +2957,99 @@ describe('parser', function() { }).toThrow(); }); - it('should prevent assigning in the context of a constructor', function() { - forEach({ - '(true)': true, - '(1)': 1, - '"string"': 'string', - '[]': [] - }, function(thing, expr) { - var constructorExpr = expr + '.constructor'; + they('should prevent assigning in the context of the $prop constructor', { + Array: [[], '[]'], + Boolean: [true, '(true)'], + Number: [1, '(1)'], + String: ['string', '"string"'] + }, function(values) { + var thing = values[0]; + var expr = values[1]; + var constructorExpr = expr + '.constructor'; - expect(function() { - scope.$eval(constructorExpr + '.join'); - }).not.toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '.join'); - }).not.toThrow(); - expect(function() { - scope.$eval(constructorExpr + '.join = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(constructorExpr + '[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); - }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(constructorExpr + '.join'); + }).not.toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '.join'); + }).not.toThrow(); + expect(function() { + scope.$eval(constructorExpr + '.join = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(constructorExpr + '[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); + }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.foo = thing; - scope.$eval('foo.constructor[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor[0] = ""', {foo: thing}); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - }); + expect(function() { + scope.foo = thing; + scope.$eval('foo.constructor[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: thing}); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + they('should prevent assigning in the context of the $prop constructor', { // These might throw different error (e.g. isecobj, isecfn), // but still having them here for good measure - forEach({ - '{}': {}, - '$eval': scope.$eval - }, function(thing, expr) { - var constructorExpr = expr + '.constructor'; + Function: [noop, '$eval'], + Object: [{}, '{}'] + }, function(values) { + var thing = values[0]; + var expr = values[1]; + var constructorExpr = expr + '.constructor'; - expect(function() { - scope.$eval(constructorExpr + '.join'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '.join'); - }).not.toThrowMinErr('$parse', 'isecaf'); - expect(function() { - scope.$eval(constructorExpr + '.join = ""'); - }).toThrow(); - expect(function() { - scope.$eval(constructorExpr + '[0] = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); - }).toThrow(); + expect(function() { + scope.$eval(constructorExpr + '.join'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '.join'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(constructorExpr + '.join = ""'); + }).toThrow(); + expect(function() { + scope.$eval(constructorExpr + '[0] = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); + }).toThrow(); - expect(function() { - scope.foo = thing; - scope.$eval('foo.constructor[0] = ""'); - }).toThrow(); - expect(function() { - delete scope.foo; - scope.$eval('foo.constructor[0] = ""', {foo: thing}); - }).toThrow(); - expect(function() { - scope.foo = thing.constructor; - scope.$eval('foo[0] = ""'); - }).toThrowMinErr('$parse', 'isecaf'); - expect(function() { - delete scope.foo; - scope.$eval('foo[0] = ""', {foo: thing.constructor}); - }).toThrowMinErr('$parse', 'isecaf'); - }); + expect(function() { + scope.foo = thing; + scope.$eval('foo.constructor[0] = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: thing}); + }).toThrow(); + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + it('should prevent assigning only in the context of an actual constructor', function() { // foo.constructor is not a constructor. expect(function() { delete scope.foo; @@ -3059,6 +3063,116 @@ describe('parser', function() { scope.$eval('"a".constructor.prototype.charCodeAt = [].concat'); }).toThrowMinErr('$parse', 'isecaf'); }); + + they('should prevent assigning in the context of the $prop constructor prototype', { + Array: [[], '[]'], + Boolean: [true, '(true)'], + Number: [1, '(1)'], + String: ['string', '"string"'] + }, function(values) { + var thing = values[0]; + var expr = values[1]; + var constructorExpr = expr + '.constructor'; + var prototypeExpr = constructorExpr + '.prototype'; + + expect(function() { + scope.$eval(prototypeExpr + '.boin'); + }).not.toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + prototypeExpr + '.boin'); + }).not.toThrow(); + expect(function() { + scope.$eval(prototypeExpr + '.boin = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(prototypeExpr + '[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo.prototype[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.foo = thing.constructor.prototype; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + + they('should prevent assigning in the context of a constructor prototype', { + // These might throw different error (e.g. isecobj, isecfn), + // but still having them here for good measure + Function: [noop, '$eval'], + Object: [{}, '{}'] + }, function(values) { + var thing = values[0]; + var expr = values[1]; + var constructorExpr = expr + '.constructor'; + var prototypeExpr = constructorExpr + '.prototype'; + + expect(function() { + scope.$eval(prototypeExpr + '.boin'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + prototypeExpr + '.boin'); + }).not.toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.$eval(prototypeExpr + '.boin = ""'); + }).toThrow(); + expect(function() { + scope.$eval(prototypeExpr + '[0] = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.prototype.boin = ""'); + }).toThrow(); + expect(function() { + delete scope.foo; + scope.$eval('foo = ' + prototypeExpr + '; foo.boin = ""'); + }).toThrow(); + + expect(function() { + scope.foo = thing.constructor; + scope.$eval('foo.prototype[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo.prototype[0] = ""', {foo: thing.constructor}); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + scope.foo = thing.constructor.prototype; + scope.$eval('foo[0] = ""'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(function() { + delete scope.foo; + scope.$eval('foo[0] = ""', {foo: thing.constructor.prototype}); + }).toThrowMinErr('$parse', 'isecaf'); + }); + + it('should prevent assigning only in the context of an actual prototype', function() { + // foo.constructor.prototype is not a constructor prototype. + expect(function() { + delete scope.foo; + scope.$eval('foo.constructor.prototype[0] = ""', {foo: {constructor: {prototype: ''}}}); + }).not.toThrow(); + }); }); it('should call the function from the received instance and not from a new one', function() { From ee3e197526ada514dbd2532e4c5630748d6ec0f8 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 26 Jul 2016 10:45:49 +0300 Subject: [PATCH 3/3] docs($parse/isecaf): add missing error page --- docs/content/error/$parse/isecaf.ngdoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/content/error/$parse/isecaf.ngdoc diff --git a/docs/content/error/$parse/isecaf.ngdoc b/docs/content/error/$parse/isecaf.ngdoc new file mode 100644 index 000000000000..115e1b26d754 --- /dev/null +++ b/docs/content/error/$parse/isecaf.ngdoc @@ -0,0 +1,12 @@ +@ngdoc error +@name $parse:isecaf +@fullName Assigning to Fields of Disallowed Context +@description + +Occurs when an expression attempts to assign a value on a field of any of the `Boolean`, `Number`, +`String`, `Array`, `Object`, or `Function` constructors or the corresponding prototypes. + +Angular bans the modification of these constructors or their prototypes from within expressions, +since it is a known way to modify the behaviour of existing functions/operations. + +To resolve this error, avoid assigning to fields of constructors or their prototypes in expressions.