diff --git a/docs/content/error/$parse/isecaf.ngdoc b/docs/content/error/$parse/isecaf.ngdoc deleted file mode 100644 index 115e1b26d754..000000000000 --- a/docs/content/error/$parse/isecaf.ngdoc +++ /dev/null @@ -1,12 +0,0 @@ -@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/docs/content/error/$parse/isecdom.ngdoc b/docs/content/error/$parse/isecdom.ngdoc deleted file mode 100644 index 9f60e189ee92..000000000000 --- a/docs/content/error/$parse/isecdom.ngdoc +++ /dev/null @@ -1,47 +0,0 @@ -@ngdoc error -@name $parse:isecdom -@fullName Referencing a DOM node in Expression -@description - -Occurs when an expression attempts to access a DOM node. - -AngularJS restricts access to DOM nodes from within expressions since it's a known way to -execute arbitrary Javascript code. - -This check is only performed on object index and function calls in Angular expressions. These are -places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not -perform this check - it's up to the developer to not expose such sensitive and powerful objects -directly on the scope chain. - -To resolve this error, avoid access to DOM nodes. - - -# Event Handlers and Return Values - -The `$parse:isecdom` error also occurs when an event handler invokes a function that returns a DOM -node. - -```html - -``` - -```js - $scope.iWillReturnDOM = function() { - return someDomNode; - } -``` - -To fix this issue, avoid returning DOM nodes from event handlers. - -*Note: This error often means that you are accessing DOM from your controllers, which is usually -a sign of poor coding style that violates separation of concerns.* - - -# Implicit Returns in CoffeeScript - -This error can occur more frequently when using CoffeeScript, which has a feature called implicit -returns. This language feature returns the last dereferenced object in the function when the -function has no explicit return statement. - -The solution in this scenario is to add an explicit return statement. For example `return false` to -the function. diff --git a/docs/content/error/$parse/isecff.ngdoc b/docs/content/error/$parse/isecff.ngdoc deleted file mode 100644 index a1e72775d254..000000000000 --- a/docs/content/error/$parse/isecff.ngdoc +++ /dev/null @@ -1,17 +0,0 @@ -@ngdoc error -@name $parse:isecff -@fullName Referencing 'call', 'apply' and 'bind' Disallowed -@description - -Occurs when an expression attempts to invoke Function's 'call', 'apply' or 'bind'. - -Angular bans the invocation of 'call', 'apply' and 'bind' from within expressions -since access is a known way to modify the behaviour of existing functions. - -To resolve this error, avoid using these methods in expressions. - -Example expression that would result in this error: - -``` -
{{user.sendInfo.call({}, true)}}
-``` diff --git a/docs/content/error/$parse/isecfld.ngdoc b/docs/content/error/$parse/isecfld.ngdoc deleted file mode 100644 index a19c5fa51e97..000000000000 --- a/docs/content/error/$parse/isecfld.ngdoc +++ /dev/null @@ -1,27 +0,0 @@ -@ngdoc error -@name $parse:isecfld -@fullName Referencing Disallowed Field in Expression -@description - -Occurs when an expression attempts to access one of the following fields: - -* __proto__ -* __defineGetter__ -* __defineSetter__ -* __lookupGetter__ -* __lookupSetter__ - -AngularJS bans access to these fields from within expressions since -access is a known way to mess with native objects or -to execute arbitrary Javascript code. - -To resolve this error, avoid using these fields in expressions. As a last resort, -alias their value and access them through the alias instead. - -Example expressions that would result in this error: - -``` -
{{user.__proto__.hasOwnProperty = $emit}}
- -
{{user.__defineGetter__('name', noop)}}
-``` diff --git a/docs/content/error/$parse/isecfn.ngdoc b/docs/content/error/$parse/isecfn.ngdoc deleted file mode 100644 index 417551cb3606..000000000000 --- a/docs/content/error/$parse/isecfn.ngdoc +++ /dev/null @@ -1,10 +0,0 @@ -@ngdoc error -@name $parse:isecfn -@fullName Referencing Function Disallowed -@description - -Occurs when an expression attempts to access the 'Function' object (constructor for all functions in JavaScript). - -Angular bans access to Function from within expressions since constructor access is a known way to execute arbitrary Javascript code. - -To resolve this error, avoid Function access. diff --git a/docs/content/error/$parse/isecobj.ngdoc b/docs/content/error/$parse/isecobj.ngdoc deleted file mode 100644 index 8da6e27a3505..000000000000 --- a/docs/content/error/$parse/isecobj.ngdoc +++ /dev/null @@ -1,11 +0,0 @@ -@ngdoc error -@name $parse:isecobj -@fullName Referencing Object Disallowed -@description - -Occurs when an expression attempts to access the 'Object' object (Root object in JavaScript). - -Angular bans access to Object from within expressions since access is a known way to modify -the behaviour of existing objects. - -To resolve this error, avoid Object access. diff --git a/docs/content/error/$parse/isecwindow.ngdoc b/docs/content/error/$parse/isecwindow.ngdoc deleted file mode 100644 index e7f4ceeaddd5..000000000000 --- a/docs/content/error/$parse/isecwindow.ngdoc +++ /dev/null @@ -1,45 +0,0 @@ -@ngdoc error -@name $parse:isecwindow -@fullName Referencing Window object in Expression -@description - -Occurs when an expression attempts to access a Window object. - -AngularJS restricts access to the Window object from within expressions since it's a known way to -execute arbitrary Javascript code. - -This check is only performed on object index and function calls in Angular expressions. These are -places that are harder for the developer to guard. Dotted member access (such as a.b.c) does not -perform this check - it's up to the developer to not expose such sensitive and powerful objects -directly on the scope chain. - -To resolve this error, avoid Window access. - -### Common CoffeeScript Issue - -Be aware that if you are using CoffeeScript, it automatically returns the value of the last statement in a -function. So for instance - -```coffeescript - scope.foo = -> - window.open 'https://example.com' -``` - -compiles to something like - -```js - scope.foo = function() { - return window.open('https://example.com'); - }; -``` - -You can see that this function will return the result of calling `window.open`, which is a `Window` -object. - -You can avoid this by explicitly returning something else from the function: - -```coffeescript - scope.foo = -> - window.open 'https://example.com' - return true; -``` diff --git a/docs/content/guide/expression.ngdoc b/docs/content/guide/expression.ngdoc index 824facd9dfb3..24326b6abae1 100644 --- a/docs/content/guide/expression.ngdoc +++ b/docs/content/guide/expression.ngdoc @@ -113,11 +113,11 @@ You can try evaluating different expressions here: Angular does not use JavaScript's `eval()` to evaluate expressions. Instead Angular's {@link ng.$parse $parse} service processes these expressions. -Angular expressions do not have access to global variables like `window`, `document` or `location`. +Angular expressions do not have direct access to global variables like `window`, `document` or `location`. This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs. -Instead use services like `$window` and `$location` in functions called from expressions. Such services -provide mockable access to globals. +Instead use services like `$window` and `$location` in functions on controllers, which are then called from expressions. +Such services provide mockable access to globals. It is possible to access the context object using the identifier `this` and the locals object using the identifier `$locals`. diff --git a/docs/content/guide/security.ngdoc b/docs/content/guide/security.ngdoc index 175c49edf9c0..9fcf0e7c6078 100644 --- a/docs/content/guide/security.ngdoc +++ b/docs/content/guide/security.ngdoc @@ -30,42 +30,55 @@ so keeping to AngularJS standards is not just a functionality issue, it's also c facilitate rapid security updates. -## Expression Sandboxing - -AngularJS's expressions are sandboxed not for security reasons, but instead to maintain a proper -separation of application responsibilities. For example, access to `window` is disallowed -because it makes it easy to introduce brittle global state into your application. - -However, this sandbox is not intended to stop attackers who can edit the template before it's -processed by Angular. It may be possible to run arbitrary JavaScript inside double-curly bindings -if an attacker can modify them. - -But if an attacker can change arbitrary HTML templates, there's nothing stopping them from doing: - -```html - -``` - -**It's better to design your application in such a way that users cannot change client-side templates.** - -For instance: +## Angular Templates and Expressions + +**If an attacker has access to control Angular templates or expressions, they can exploit an Angular application +via an XSS attack, regardless of the version.** + +There are a number of ways that templates and expressions can be controlled: + +* **Generating Angular templates on the server containing user-provided content**. This is the most common pitfall + where you are generating HTML via some server-side engine such as PHP, Java or ASP.NET. +* **Passing an expression generated from user-provided content in calls to the following methods on a {@link scope scope}**: + * `$watch(userContent, ...)` + * `$watchGroup(userContent, ...)` + * `$watchCollection(userContent, ...)` + * `$eval(userContent)` + * `$evalAsync(userContent)` + * `$apply(userContent)` + * `$applyAsync(userContent)` +* **Passing an expression generated from user-provided content in calls to services that parse expressions**: + * `$compile(userContent)` + * `$parse(userContent)` + * `$interpolate(userContent)` +* **Passing an expression generated from user provided content as a predicate to `orderBy` pipe**: + `{{ value | orderBy : userContent }}` + +### Sandbox removal +Each version of Angular 1 up to, but not including 1.6, contained an expression sandbox, which reduced the surface area of +the vulnerability but never removed it. **In Angular 1.6 we removed this sandbox as developers kept relying upon it as a security +feature even though it was always possible to access arbitrary JavaScript code if one could control the Angular templates +or expressions of applications.** + +Control of the Angular templates makes applications vulnerable even if there was a completely secure sandbox: +* https://ryhanson.com/stealing-session-tokens-on-plunker-with-an-angular-expression-injection/ in this blog post the author shows + a (now closed) vulnerability in the Plunker application due to server-side rendering inside an Angular template. +* https://ryhanson.com/angular-expression-injection-walkthrough/ in this blog post the author describes an attack, which does not + rely upon an expression sandbox bypass, that can be made because the sample application is rendering a template on the server that + contains user entered content. + +**It's best to design your application in such a way that users cannot change client-side templates.** * Do not mix client and server templates * Do not use user input to generate templates dynamically -* Do not run user input through `$scope.$eval` +* Do not run user input through `$scope.$eval` (or any of the other expression parsing functions listed above) * Consider using {@link ng.directive:ngCsp CSP} (but don't rely only on CSP) +**You can use suitably sanitized server-side templating to dynamically generate CSS, URLs, etc, but not for generating templates that are +bootstrapped/compiled by Angular.** -### Mixing client-side and server-side templates - -In general, we recommend against this because it can create unintended XSS vectors. - -However, it's ok to mix server-side templating in the bootstrap template (`index.html`) as long -as user input cannot be used on the server to output html that would then be processed by Angular -in a way that would allow for arbitrary code execution. - -**For instance, you can use server-side templating to dynamically generate CSS, URLs, etc, but not -for generating templates that are bootstrapped/compiled by Angular.** +**If you must continue to allow user-provided content in an Angular template then the safest option is to ensure that it is only +present in the part of the template that is made inert via the {@link ngNonBindable} directive.** ## HTTP Requests diff --git a/src/ng/parse.js b/src/ng/parse.js index a008dd1e4fea..ed0c4ca43afc 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -13,60 +13,23 @@ var $parseMinErr = minErr('$parse'); -var ARRAY_CTOR = [].constructor; -var BOOLEAN_CTOR = (false).constructor; -var FUNCTION_CTOR = Function.constructor; -var NUMBER_CTOR = (0).constructor; -var OBJECT_CTOR = {}.constructor; -var STRING_CTOR = ''.constructor; -var ARRAY_CTOR_PROTO = ARRAY_CTOR.prototype; -var BOOLEAN_CTOR_PROTO = BOOLEAN_CTOR.prototype; -var FUNCTION_CTOR_PROTO = FUNCTION_CTOR.prototype; -var NUMBER_CTOR_PROTO = NUMBER_CTOR.prototype; -var OBJECT_CTOR_PROTO = OBJECT_CTOR.prototype; -var STRING_CTOR_PROTO = STRING_CTOR.prototype; - -var CALL = FUNCTION_CTOR_PROTO.call; -var APPLY = FUNCTION_CTOR_PROTO.apply; -var BIND = FUNCTION_CTOR_PROTO.bind; - -var objectValueOf = OBJECT_CTOR_PROTO.valueOf; +var objectValueOf = {}.constructor.prototype.valueOf; // Sandboxing Angular Expressions // ------------------------------ -// 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 -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. -// Similarly we prevent invocations of function known to be dangerous, as well as assignments to -// native objects. +// It is important to realise that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === '__defineGetter__' || name === '__defineSetter__' - || name === '__lookupGetter__' || name === '__lookupSetter__' - || name === '__proto__') { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted @@ -85,67 +48,6 @@ function getStringValue(name) { return name + ''; } -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } -} - -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === ARRAY_CTOR || - obj === BOOLEAN_CTOR || - obj === FUNCTION_CTOR || - obj === NUMBER_CTOR || - obj === OBJECT_CTOR || - obj === STRING_CTOR || - obj === ARRAY_CTOR_PROTO || - obj === BOOLEAN_CTOR_PROTO || - obj === FUNCTION_CTOR_PROTO || - obj === NUMBER_CTOR_PROTO || - obj === OBJECT_CTOR_PROTO || - obj === STRING_CTOR_PROTO) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor or its prototype is disallowed! Expression: {0}', - fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); @@ -862,13 +764,12 @@ function ASTCompiler(astBuilder, $filter) { } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, - expensiveChecks: expensiveChecks, fn: {vars: [], body: [], own: {}}, assign: {vars: [], body: [], own: {}}, inputs: [] @@ -911,21 +812,13 @@ ASTCompiler.prototype = { // eslint-disable-next-line no-new-func var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', 'text', fnString))( this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, getStringValue, - ensureSafeAssignContext, ifDefined, plusFn, expression); @@ -1042,7 +935,6 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - ensureSafeMemberName(ast.name); self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if_(self.stage === 'inputs' || 's', function() { @@ -1055,9 +947,6 @@ ASTCompiler.prototype = { }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -1065,32 +954,24 @@ ASTCompiler.prototype = { intoId = intoId || this.nextId(); self.recurse(ast.object, left, undefined, function() { self.if_(self.notNull(left), function() { - if (create && create !== 1) { - self.addEnsureSafeAssignContext(left); - } if (ast.computed) { right = self.nextId(); self.recurse(ast.property, right); self.getStringValue(right); - self.addEnsureSafeMemberName(right); if (create && create !== 1) { self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); } - expression = self.ensureSafeObject(self.computedMember(left, right)); + expression = self.computedMember(left, right); self.assign(intoId, expression); if (nameId) { nameId.computed = true; nameId.name = right; } } else { - ensureSafeMemberName(ast.property.name); if (create && create !== 1) { self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -1122,21 +1003,16 @@ ASTCompiler.prototype = { args = []; self.recurse(ast.callee, right, left, function() { self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); forEach(ast.arguments, function(expr) { self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); + args.push(argument); }); }); if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; } else { expression = right + '(' + args.join(',') + ')'; } - expression = self.ensureSafeObject(expression); self.assign(intoId, expression); }, function() { self.assign(intoId, 'undefined'); @@ -1154,8 +1030,6 @@ ASTCompiler.prototype = { this.recurse(ast.left, undefined, left, function() { self.if_(self.notNull(left.context), function() { self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - self.addEnsureSafeAssignContext(left.context); expression = self.member(left.context, left.name, left.computed) + ast.operator + right; self.assign(intoId, expression); recursionFn(intoId || expression); @@ -1303,42 +1177,10 @@ ASTCompiler.prototype = { return this.nonComputedMember(left, right); }, - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - getStringValue: function(item) { this.assign(item, 'getStringValue(' + item + ')'); }, - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; - }, - lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { var self = this; return function() { @@ -1390,11 +1232,10 @@ function ASTInterpreter(astBuilder, $filter) { } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -1465,20 +1306,17 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), context, create, self.expression); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); right = ast.property.name; } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + this.nonComputedMember(left, right, context, create, self.expression); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -1499,13 +1337,11 @@ ASTInterpreter.prototype = { var rhs = right(scope, locals, assign, inputs); var value; if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); var values = []; for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -1515,8 +1351,6 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - ensureSafeAssignContext(lhs.context); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; @@ -1708,16 +1542,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create, expression) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; if (create && create !== 1 && base && !(base[name])) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -1733,15 +1564,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[rhs])) { lhs[rhs] = {}; } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -1750,19 +1578,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create, expression) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); if (create && create !== 1) { - ensureSafeAssignContext(lhs); if (lhs && !(lhs[right])) { lhs[right] = {}; } } var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } if (context) { return {context: lhs, name: right, value: value}; } else { @@ -1794,14 +1618,10 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + return this.astCompiler.compile(text); } }; -function isPossiblyDangerousMemberName(name) { - return name === 'constructor'; -} - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -1859,8 +1679,7 @@ function getValueOf(value) { * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); var literals = { 'true': true, 'false': false, @@ -1918,37 +1737,20 @@ function $ParseProvider() { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false, - literals: copy(literals), - isIdentifierStart: isFunction(identStart) && identStart, - isIdentifierContinue: isFunction(identContinue) && identContinue - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true, literals: copy(literals), isIdentifierStart: isFunction(identStart) && identStart, isIdentifierContinue: isFunction(identContinue) && identContinue }; - var runningChecksEnabled = false; - - $parse.$$runningExpensiveChecks = function() { - return runningChecksEnabled; - }; - return $parse; - function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; - expensiveChecks = expensiveChecks || runningChecksEnabled; - switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { @@ -1956,9 +1758,8 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; - var lexer = new Lexer(parseOptions); - var parser = new Parser(lexer, $filter, parseOptions); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; @@ -1968,9 +1769,6 @@ function $ParseProvider() { } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - if (expensiveChecks) { - parsedExpression = expensiveChecksInterceptor(parsedExpression); - } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -1983,30 +1781,6 @@ function $ParseProvider() { } } - function expensiveChecksInterceptor(fn) { - if (!fn) return fn; - expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; - expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); - expensiveCheckFn.constant = fn.constant; - expensiveCheckFn.literal = fn.literal; - for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { - fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); - } - expensiveCheckFn.inputs = fn.inputs; - - return expensiveCheckFn; - - function expensiveCheckFn(scope, locals, assign, inputs) { - var expensiveCheckOldValue = runningChecksEnabled; - runningChecksEnabled = true; - try { - return fn(scope, locals, assign, inputs); - } finally { - runningChecksEnabled = expensiveCheckOldValue; - } - } - } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { if (newValue == null || oldValueOfValue == null) { // null/undefined diff --git a/test/ng/directive/ngEventDirsSpec.js b/test/ng/directive/ngEventDirsSpec.js index e2c2745f840e..d8288b828bbd 100644 --- a/test/ng/directive/ngEventDirsSpec.js +++ b/test/ng/directive/ngEventDirsSpec.js @@ -90,23 +90,13 @@ describe('event directives', function() { }); - describe('security', function() { + describe('DOM event object', function() { it('should allow access to the $event object', inject(function($rootScope, $compile) { var scope = $rootScope.$new(); element = $compile('')(scope); element.triggerHandler('click'); expect(scope.e.target).toBe(element[0]); })); - - it('should block access to DOM nodes (e.g. exposed via $event)', inject(function($rootScope, $compile) { - var scope = $rootScope.$new(); - element = $compile('')(scope); - expect(function() { - element.triggerHandler('click'); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! ' + - 'Expression: e = $event.target'); - })); }); describe('blur', function() { diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js index ea216a644c75..79d49340da46 100644 --- a/test/ng/parseSpec.js +++ b/test/ng/parseSpec.js @@ -2422,770 +2422,6 @@ describe('parser', function() { })); - describe('sandboxing', function() { - describe('Function constructor', function() { - it('should not tranverse the Function constructor in the getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor'); - }); - - it('should not allow access to the Function prototype in the getter', function() { - expect(function() { - scope.$eval('toString.constructor.prototype'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: toString.constructor.prototype'); - }); - - it('should NOT allow access to Function constructor in getter', function() { - expect(function() { - scope.$eval('{}.toString.constructor("alert(1)")'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor("alert(1)")'); - }); - - it('should NOT allow access to Function constructor in setter', function() { - - expect(function() { - scope.$eval('{}.toString.constructor.a = 1'); - }).toThrowMinErr( - '$parse', 'isecfn','Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString.constructor.a = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["constructor"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["constructor"] = 1'); - - scope.key1 = 'const'; - scope.key2 = 'ructor'; - expect(function() { - scope.$eval('{}.toString[key1 + key2].foo = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString[key1 + key2].foo = 1'); - - expect(function() { - scope.$eval('{}.toString["constructor"]["a"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: {}.toString["constructor"]["a"] = 1'); - - scope.a = []; - expect(function() { - scope.$eval('a.toString.constructor = 1', scope); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: a.toString.constructor'); - }); - - it('should disallow traversing the Function object in a setter: E02', function() { - expect(function() { - // This expression by itself isn't dangerous. However, one can use this to - // automatically call an object (e.g. a Function object) when it is automatically - // toString'd/valueOf'd by setting the RHS to Function.prototype.call. - scope.$eval('hasOwnProperty.constructor.prototype.valueOf = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: hasOwnProperty.constructor.prototype.valueOf'); - }); - - it('should disallow passing the Function object as a parameter: E03', function() { - expect(function() { - // This expression constructs a function but does not execute it. It does lead the - // way to execute it if one can get the toString/valueOf of it to call the function. - scope.$eval('["a", "alert(1)"].sort(hasOwnProperty.constructor)'); - }).toThrow(); - }); - - it('should prevent exploit E01', function() { - // This is a tracking exploit. The two individual tests, it('should … : E02') and - // it('should … : E03') test for two parts to block this exploit. This exploit works - // as follows: - // - // • Array.sort takes a comparison function and passes it 2 parameters to compare. If - // the result is non-primitive, sort then invokes valueOf() on the result. - // • The Function object conveniently accepts two string arguments so we can use this - // to construct a function. However, this doesn't do much unless we can execute it. - // • We set the valueOf property on Function.prototype to Function.prototype.call. - // This causes the function that we constructed to be executed when sort calls - // .valueOf() on the result of the comparison. - expect(function() { - scope.$eval('' + - 'hasOwnProperty.constructor.prototype.valueOf=valueOf.call;' + - '["a","alert(1)"].sort(hasOwnProperty.constructor)'); - }).toThrow(); - }); - - it('should NOT allow access to Function constructor that has been aliased in getters', function() { - scope.foo = { 'bar': Function }; - expect(function() { - scope.$eval('foo["bar"]'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]'); - }); - - it('should NOT allow access to Function constructor that has been aliased in setters', function() { - scope.foo = { 'bar': Function }; - expect(function() { - scope.$eval('foo["bar"] = 1'); - }).toThrowMinErr( - '$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' + - 'Expression: foo["bar"] = 1'); - }); - - describe('expensiveChecks', function() { - it('should block access to window object even when aliased in getters', inject(function($parse, $window) { - scope.foo = {w: $window}; - // This isn't blocked for performance. - expect(scope.$eval($parse('foo.w'))).toBe($window); - // Event handlers use the more expensive path for better protection since they expose - // the $event object on the scope. - expect(function() { - scope.$eval($parse('foo.w', null, true)); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w'); - })); - - it('should block access to window object even when aliased in setters', inject(function($parse, $window) { - scope.foo = {w: $window}; - // This is blocked as it points to `window`. - expect(function() { - expect(scope.$eval($parse('foo.w = 1'))).toBe($window); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w = 1'); - // Event handlers use the more expensive path for better protection since they expose - // the $event object on the scope. - expect(function() { - scope.$eval($parse('foo.w = 1', null, true)); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w = 1'); - })); - - they('should propagate expensive checks when calling $prop', - ['foo.w && true', - '$eval("foo.w && true")', - 'this["$eval"]("foo.w && true")', - 'bar;$eval("foo.w && true")', - '$eval("foo.w && true");bar', - '$eval("foo.w && true", null, false)', - '$eval("foo");$eval("foo.w && true")', - '$eval("$eval(\\"foo.w && true\\")")', - '$eval("foo.e()")', - '$evalAsync("foo.w && true")', - 'this["$evalAsync"]("foo.w && true")', - 'bar;$evalAsync("foo.w && true")', - '$evalAsync("foo.w && true");bar', - '$evalAsync("foo.w && true", null, false)', - '$evalAsync("foo");$evalAsync("foo.w && true")', - '$evalAsync("$evalAsync(\\"foo.w && true\\")")', - '$evalAsync("foo.e()")', - '$evalAsync("$eval(\\"foo.w && true\\")")', - '$eval("$evalAsync(\\"foo.w && true\\")")', - '$watch("foo.w && true")', - '$watchCollection("foo.w && true", foo.f)', - '$watchGroup(["foo.w && true"])', - '$applyAsync("foo.w && true")'], function(expression) { - inject(function($parse, $window) { - scope.foo = { - w: $window, - bar: 'bar', - e: function() { scope.$eval('foo.w && true'); }, - f: function() {} - }; - expect($parse.$$runningExpensiveChecks()).toEqual(false); - expect(function() { - scope.$eval($parse(expression, null, true)); - scope.$digest(); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' + - 'Expression: foo.w && true'); - expect($parse.$$runningExpensiveChecks()).toEqual(false); - }); - }); - - they('should restore the state of $$runningExpensiveChecks when the expression $prop throws', - ['$eval("foo.t()")', - '$evalAsync("foo.t()", {foo: foo})'], function(expression) { - inject(function($parse, $window) { - scope.foo = { - t: function() { throw new Error(); } - }; - expect($parse.$$runningExpensiveChecks()).toEqual(false); - expect(function() { - scope.$eval($parse(expression, null, true)); - scope.$digest(); - }).toThrow(); - expect($parse.$$runningExpensiveChecks()).toEqual(false); - }); - }); - - it('should handle `inputs` when running with expensive checks', inject(function($parse) { - expect(function() { - scope.$watch($parse('a + b', null, true), noop); - scope.$digest(); - }).not.toThrow(); - })); - }); - }); - - describe('Function prototype functions', function() { - it('should NOT allow invocation to Function.call', function() { - scope.fn = Function.prototype.call; - - expect(function() { - scope.$eval('$eval.call()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.call()'); - - expect(function() { - scope.$eval('fn()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: fn()'); - }); - - it('should NOT allow invocation to Function.apply', function() { - scope.apply = Function.prototype.apply; - - expect(function() { - scope.$eval('$eval.apply()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.apply()'); - - expect(function() { - scope.$eval('apply()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: apply()'); - }); - - it('should NOT allow invocation to Function.bind', function() { - scope.bind = Function.prototype.bind; - - expect(function() { - scope.$eval('$eval.bind()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: $eval.bind()'); - - expect(function() { - scope.$eval('bind()'); - }).toThrowMinErr( - '$parse', 'isecff', 'Referencing call, apply or bind in Angular expressions is disallowed! ' + - 'Expression: bind()'); - }); - }); - - describe('Object constructor', function() { - - it('should NOT allow access to Object constructor that has been aliased in getters', function() { - scope.foo = { 'bar': Object }; - - expect(function() { - scope.$eval('foo.bar.keys(foo)'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo.bar.keys(foo)'); - - expect(function() { - scope.$eval('foo["bar"]["keys"](foo)'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]["keys"](foo)'); - }); - - it('should NOT allow access to Object constructor that has been aliased in setters', function() { - scope.foo = { 'bar': Object }; - - expect(function() { - scope.$eval('foo.bar.keys(foo).bar = 1'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo.bar.keys(foo).bar = 1'); - - expect(function() { - scope.$eval('foo["bar"]["keys"](foo).bar = 1'); - }).toThrowMinErr( - '$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' + - 'Expression: foo["bar"]["keys"](foo).bar = 1'); - }); - }); - - describe('Window and $element/node', function() { - it('should NOT allow access to the Window or DOM when indexing', inject(function($window, $document) { - scope.wrap = {w: $window, d: $document}; - - expect(function() { - scope.$eval('wrap["w"]', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: wrap["w"]'); - expect(function() { - scope.$eval('wrap["d"]', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: wrap["d"]'); - expect(function() { - scope.$eval('wrap["w"] = 1', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: wrap["w"] = 1'); - expect(function() { - scope.$eval('wrap["d"] = 1', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: wrap["d"] = 1'); - })); - - it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) { - scope.getWin = valueFn($window); - scope.getDoc = valueFn($document); - - expect(function() { - scope.$eval('getWin()', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: getWin()'); - expect(function() { - scope.$eval('getDoc()', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: getDoc()'); - })); - - it('should NOT allow calling functions on Window or DOM', inject(function($window, $document) { - scope.a = {b: { win: $window, doc: $document }}; - expect(function() { - scope.$eval('a.b.win.alert(1)', scope); - }).toThrowMinErr( - '$parse', 'isecwindow', 'Referencing the Window in Angular expressions is ' + - 'disallowed! Expression: a.b.win.alert(1)'); - expect(function() { - scope.$eval('a.b.doc.on("click")', scope); - }).toThrowMinErr( - '$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is ' + - 'disallowed! Expression: a.b.doc.on("click")'); - })); - - // Issue #4805 - it('should NOT throw isecdom when referencing a Backbone Collection', function() { - // Backbone stuff is sort of hard to mock, if you have a better way of doing this, - // please fix this. - var fakeBackboneCollection = { - children: [{}, {}, {}], - find: function() {}, - on: function() {}, - off: function() {}, - bind: function() {} - }; - scope.backbone = fakeBackboneCollection; - expect(function() { scope.$eval('backbone'); }).not.toThrow(); - }); - - it('should NOT throw isecdom when referencing an array with node properties', function() { - var array = [1,2,3]; - array.on = array.attr = array.prop = array.bind = true; - scope.array = array; - expect(function() { scope.$eval('array'); }).not.toThrow(); - }); - }); - - describe('Disallowed fields', function() { - it('should NOT allow access or invocation of __defineGetter__', function() { - expect(function() { - scope.$eval('{}.__defineGetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__defineGetter__("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__defineGetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__defineGetter__"]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__define'; - scope.b = 'Getter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __defineSetter__', function() { - expect(function() { - scope.$eval('{}.__defineSetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__defineSetter__("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__defineSetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__defineSetter__"]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__define'; - scope.b = 'Setter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a", "".charAt)'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __lookupGetter__', function() { - expect(function() { - scope.$eval('{}.__lookupGetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__lookupGetter__("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__lookupGetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__lookupGetter__"]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__lookup'; - scope.b = 'Getter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access or invocation of __lookupSetter__', function() { - expect(function() { - scope.$eval('{}.__lookupSetter__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__lookupSetter__("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__lookupSetter__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__lookupSetter__"]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__lookup'; - scope.b = 'Setter__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b]("a")'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - - it('should NOT allow access to __proto__', function() { - expect(function() { - scope.$eval('__proto__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__proto__'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}.__proto__.foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}["__proto__"]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}["__proto__"].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('{}[["__proto__"]]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[["__proto__"]].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - expect(function() { - scope.$eval('0[["__proto__"]]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('0[["__proto__"]].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - - scope.a = '__pro'; - scope.b = 'to__'; - expect(function() { - scope.$eval('{}[a + b]'); - }).toThrowMinErr('$parse', 'isecfld'); - expect(function() { - scope.$eval('{}[a + b].foo = 1'); - }).toThrowMinErr('$parse', 'isecfld'); - }); - }); - - it('should prevent the exploit', function() { - expect(function() { - scope.$eval('(1)[{0: "__proto__", 1: "__proto__", 2: "__proto__", 3: "safe", length: 4, toString: [].pop}].foo = 1'); - }).toThrow(); - if (!msie || msie > 10) { - // eslint-disable-next-line no-proto - expect((1)['__proto__'].foo).toBeUndefined(); - } - }); - - it('should prevent the exploit', function() { - expect(function() { - scope.$eval('' + - ' "".sub.call.call(' + - '({})["constructor"].getOwnPropertyDescriptor("".sub.__proto__, "constructor").value,' + - 'null,' + - '"alert(1)"' + - ')()' + - ''); - }).toThrow(); - }); - - 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.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(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'); - }); - - it('should prevent assigning only in the context of an actual constructor', function() { - // foo.constructor is not a constructor. - expect(function() { - 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(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() { var n = 0; scope.fn = function() {