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. 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 116bd9f06de2..230defc30a17 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2957,39 +2957,221 @@ describe('parser', function() { }).toThrow(); }); - it('should prevent assigning in the context of a constructor', function() { + 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() { - scope.$eval("''.constructor.join"); + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '.join'); }).not.toThrow(); expect(function() { - scope.$eval("''.constructor.join = ''.constructor.join"); + 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'); + }); + + 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 + 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("''.constructor[0] = ''"); + scope.$eval(constructorExpr + '[0] = ""'); }).toThrow(); expect(function() { - scope.$eval("(0).constructor[0] = ''"); + delete scope.foo; + scope.$eval('foo = ' + constructorExpr + '; foo.join = ""'); }).toThrow(); + expect(function() { - scope.$eval("{}.constructor[0] = ''"); + scope.foo = thing; + scope.$eval('foo.constructor[0] = ""'); }).toThrow(); - // foo.constructor is the object constructor. expect(function() { - scope.$eval("foo.constructor[0] = ''", {foo: {}}); + 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() { - scope.$eval("foo.constructor[0] = ''", {foo: {constructor: ''}}); + delete scope.foo; + scope.$eval('foo.constructor[0] = ""', {foo: {constructor: ''}}); + }).not.toThrow(); + + expect(function() { + scope.$eval('"a".constructor.prototype.charAt = [].join'); + }).toThrowMinErr('$parse', 'isecaf'); + expect(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("objConstructor = {}.constructor; objConstructor.join = ''"); + 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("'a'.constructor.prototype.charAt=[].join"); + scope.$eval(prototypeExpr + '[0] = ""'); }).toThrow(); expect(function() { - scope.$eval("'a'.constructor.prototype.charCodeAt=[].concat"); + 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(); }); });