refactor($parse): remove Angular expression sandbox

The angular expression parser (`$parse`) attempts to sandbox expressions
to prevent unrestricted access to the global context.

While the sandbox was not on the frontline of the security defense,
developers kept relying upon it as a security feature even though it was
always possible to access arbitrary JavaScript code if a malicious user
could control the content of Angular templates in applications.

This commit removes this sandbox, which has the following benefits:

* it sends a clear message to developers that they should not rely on
the sandbox to prevent XSS attacks; that they must prevent control of
expression and templates instead.
* it allows performance and size improvements in the core Angular 1
library.
* it simplifies maintenance and provides opportunities to make the
parser more capable.

Please see the [Sandbox Removal Blog Post](http://angularjs.blogspot.com/2016/09/angular-16-expression-sandbox-removal.html)
for more detail on what you should do to ensure that your application is
secure.

Closes #15094
This commit is contained in:
Peter Bacon Darwin
2016-09-06 14:33:23 +01:00
parent 76d3dafdea
commit 1547c751aa
12 changed files with 60 additions and 1216 deletions
-12
View File
@@ -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.
-47
View File
@@ -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
<button ng-click="iWillReturnDOM()">click me</button>
```
```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.
-17
View File
@@ -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:
```
<div>{{user.sendInfo.call({}, true)}}</div>
```
-27
View File
@@ -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:
```
<div>{{user.__proto__.hasOwnProperty = $emit}}</div>
<div>{{user.__defineGetter__('name', noop)}}</div>
```
-10
View File
@@ -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.
-11
View File
@@ -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.
@@ -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;
```
+3 -3
View File
@@ -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`.
+37 -24
View File
@@ -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
## Angular Templates and Expressions
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.
**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.**
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.
There are a number of ways that templates and expressions can be controlled:
But if an attacker can change arbitrary HTML templates, there's nothing stopping them from doing:
* **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 }}`
```html
<script>somethingEvil();</script>
```
### 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.**
**It's better to design your application in such a way that users cannot change client-side templates.**
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.
For instance:
**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
+19 -245
View File
@@ -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
+1 -11
View File
@@ -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('<button ng-click="e = $event">BTN</button>')(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('<button ng-click="e = $event.target">BTN</button>')(scope);
expect(function() {
element.triggerHandler('click');
}).toThrowMinErr(
'$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! ' +
'Expression: e = $event.target');
}));
});
describe('blur', function() {
-764
View File
@@ -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() {