refactor($parse): move around previous security changes made to $parse

This commit is contained in:
rodyhaddad
2014-06-26 15:24:10 -07:00
committed by Igor Minar
parent 6081f20769
commit db713a1c1b
4 changed files with 148 additions and 206 deletions

View File

@@ -1,18 +1,27 @@
@ngdoc error
@name $parse:isecfld
@fullName Referencing 'constructor' Field in Expression
@fullName Referencing Disallowed Field in Expression
@description
Occurs when an expression attempts to access an objects constructor field.
Occurs when an expression attempts to access one of the following fields:
AngularJS bans constructor access from within expressions since constructor
access is a known way to execute arbitrary Javascript code.
* __proto__
* __defineGetter__
* __defineSetter__
* __lookupGetter__
* __lookupSetter__
To resolve this error, avoid constructor access. As a last resort, alias
the constructor and access it through the alias instead.
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.
Example expression that would result in this error:
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.constructor.name}}</div>
```
<div>{{user.__proto__.hasOwnProperty = $emit}}</div>
<div>{{user.__defineGetter__('name', noop)}}</div>
```

View File

@@ -0,0 +1,11 @@
@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.

View File

@@ -10,14 +10,7 @@ var $parseMinErr = minErr('$parse');
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor(alert("evil JS code"))
//
// We want to prevent this type of access. For the sake of performance, during the lexing phase we
// disallow any "dotted" access to any member named "constructor".
//
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor
// while evaluating the expression, which is a stronger but more expensive test. Since reflective
// calls are expensive anyway, this is not such a big deal compared to static dereferencing.
// {}.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
@@ -25,24 +18,18 @@ var $parseMinErr = minErr('$parse');
// practice and therefore we are not even trying to protect against interaction with an object
// explicitly exposed in this way.
//
// A developer could foil the name check by aliasing the Function constructor under a different
// name on the scope.
//
// 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.
function ensureSafeMemberName(name, fullExpression) {
if (name === "constructor") {
if (name === "__defineGetter__" || name === "__defineSetter__"
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|| name === "__proto__") {
throw $parseMinErr('isecfld',
'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}',
fullExpression);
} else if (name === "__defineGetter__" || name === "__defineSetter__"
|| name === "__lookupGetter__" || name === "__lookupSetter__") {
throw $parseMinErr('isecgetset',
'Defining and looking up getters and setters in Angular expressions is disallowed! '
+'Expression: {0}', fullExpression);
} else if (name === "__proto__") {
throw $parseMinErr('isecproto', 'Using __proto__ in Angular expressions is disallowed! '
'Attempting to access a disallowed field in Angular expressions! '
+'Expression: {0}', fullExpression);
}
return name;
@@ -56,7 +43,7 @@ function ensureSafeObject(obj, fullExpression) {
'Referencing Function in Angular expressions is disallowed! Expression: {0}',
fullExpression);
} else if (// isWindow(obj)
obj.document && obj.location && obj.alert && obj.setInterval) {
obj.window === obj) {
throw $parseMinErr('isecwindow',
'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
fullExpression);
@@ -65,21 +52,26 @@ function ensureSafeObject(obj, fullExpression) {
throw $parseMinErr('isecdom',
'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
fullExpression);
} else if (// isObject(obj)
obj.getOwnPropertyNames || obj.getOwnPropertyDescriptor) {
} 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);
} else if (obj === ({}).__defineGetter__ || obj === ({}).__defineSetter__
|| obj === ({}).__lookupGetter__ || obj === ({}).__lookupSetter__) {
throw $parseMinErr('isecgetset',
'Defining and looking up getters and setters 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);
}
}
}
var OPERATORS = {
/* jshint bitwise : false */
'null':function(){return null;},
@@ -699,10 +691,7 @@ Parser.prototype = {
i = indexFn(self, locals),
v;
if (i === "__proto__") {
throw $parseMinErr('isecproto', 'Using __proto__ in Angular expressions is disallowed! '
+'Expression: {0}', parser.text);
}
ensureSafeMemberName(i, parser.text);
if (!o) return undefined;
v = ensureSafeObject(o[i], parser.text);
return v;
@@ -737,7 +726,7 @@ Parser.prototype = {
var fnPtr = fn(scope, locals, context) || noop;
ensureSafeObject(context, parser.text);
ensureSafeObject(fnPtr, parser.text);
ensureSafeFunction(fnPtr, parser.text);
// IE stupidity! (IE doesn't have apply for some native functions)
var v = fnPtr.apply
@@ -832,6 +821,8 @@ function setter(obj, path, setValue, fullExp) {
obj = propertyObj;
}
key = ensureSafeMemberName(element.shift(), fullExp);
ensureSafeObject(obj, fullExp);
ensureSafeObject(obj[key], fullExp);
obj[key] = setValue;
return setValue;
}

View File

@@ -638,59 +638,21 @@ describe('parser', function() {
describe('sandboxing', function() {
describe('Function constructor', function() {
it('should NOT allow access to Function constructor in getter', function() {
expect(function() {
scope.$eval('{}.toString.constructor');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor');
expect(function() {
scope.$eval('{}.toString.constructor("alert(1)")');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor("alert(1)")');
expect(function() {
scope.$eval('[].toString.constructor.foo');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: [].toString.constructor.foo');
expect(function() {
scope.$eval('{}.toString["constructor"]');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString["constructor"]');
expect(function() {
scope.$eval('{}["toString"]["constructor"]');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}["toString"]["constructor"]');
scope.a = [];
expect(function() {
scope.$eval('a.toString.constructor', scope);
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: a.toString.constructor');
expect(function() {
scope.$eval('a.toString["constructor"]', scope);
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: a.toString["constructor"]');
});
it('should NOT allow access to Function constructor in setter', function() {
expect(function() {
scope.$eval('{}.toString.constructor = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor = 1');
expect(function() {
scope.$eval('{}.toString.constructor.a = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn','Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor.a = 1');
expect(function() {
@@ -699,14 +661,13 @@ describe('parser', function() {
'$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');
'Expression: {}.toString[key1 + key2].foo = 1');
expect(function() {
scope.$eval('{}.toString["constructor"]["a"] = 1');
@@ -718,7 +679,7 @@ describe('parser', function() {
expect(function() {
scope.$eval('a.toString.constructor = 1', scope);
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: a.toString.constructor = 1');
});
@@ -732,42 +693,24 @@ describe('parser', function() {
'Expression: foo["bar"]');
});
it('should NOT allow access to Function constructor in getter', function() {
expect(function() {
scope.$eval('{}.toString.constructor');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString.constructor');
});
});
describe('Object constructor', function() {
it('should NOT allow access to scope constructor', function() {
expect(function() {
scope.$eval('constructor.keys({})');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions '+
'is disallowed! Expression: constructor.keys({})');
});
it('should NOT allow access to Object constructor in getter', function() {
expect(function() {
scope.$eval('{}["constructor"]');
}).toThrowMinErr(
'$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
'Expression: {}["constructor"]');
});
it('should NOT allow access to Object constructor that has been aliased', function() {
scope.foo = { "bar": Object };
expect(function() {
scope.$eval('foo["bar"]');
scope.$eval('foo.bar.keys(foo)');
}).toThrowMinErr(
'$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
'Expression: foo["bar"]');
'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)');
});
});
@@ -841,126 +784,114 @@ describe('parser', function() {
});
});
describe('getters and setters', function() {
it('should NOT allow invocation of __defineGetter__', function() {
expect(function() {
scope.$eval('{}.__defineGetter__("a", "".charAt)');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__defineGetter__("a", "".charAt)');
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__.call({}, "a", "".charAt)');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__defineGetter__.call({}, "a", "".charAt)');
expect(function() {
scope.$eval('{}["__defineGetter__"]'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}["__defineGetter__"]("a", "".charAt)'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}["__defineGetter__"].call({}, "a", "".charAt)');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}["__defineGetter__"].call({}, "a", "".charAt)');
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 invocation of __defineSetter__', function() {
it('should NOT allow access or invocation of __defineSetter__', function() {
expect(function() {
scope.$eval('{}.__defineSetter__("a", "".charAt)');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__defineSetter__("a", "".charAt)');
scope.$eval('{}.__defineSetter__'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__defineSetter__("a", "".charAt)'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__defineSetter__.call({}, "a", "".charAt)');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__defineSetter__.call({}, "a", "".charAt)');
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 invocation of __lookupGetter__', function() {
it('should NOT allow access or invocation of __lookupGetter__', function() {
expect(function() {
scope.$eval('{}.__lookupGetter__("a")');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__lookupGetter__("a")');
scope.$eval('{}.__lookupGetter__'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__lookupGetter__("a")'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__lookupGetter__.call({}, "a")');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__lookupGetter__.call({}, "a")');
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 invocation of __lookupSetter__', function() {
it('should NOT allow access or invocation of __lookupSetter__', function() {
expect(function() {
scope.$eval('{}.__lookupSetter__("a")');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__lookupSetter__("a")');
scope.$eval('{}.__lookupSetter__'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__lookupSetter__("a")'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}.__lookupSetter__.call({}, "a")');
}).toThrowMinErr(
'$parse', 'isecgetset', 'Defining and looking up getters and setters in '+
'Angular expressions is disallowed! Expression: '+
'{}.__lookupSetter__.call({}, "a")');
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');
});
});
describe('__proto__', function() {
it('should NOT allow access to __proto__', function() {
expect(function() {
scope.$eval('{}.__proto__.foo = 1');
}).toThrowMinErr(
'$parse', 'isecproto', 'Using __proto__ in Angular expressions is disallowed!'+
' Expression: {}.__proto__.foo = 1');
scope.$eval('{}.__proto__'); }).toThrowMinErr('$parse', 'isecfld');
expect(function() {
scope.$eval('{}["__pro"+"to__"].foo = 1');
}).toThrowMinErr(
'$parse', 'isecproto', 'Using __proto__ in Angular expressions is disallowed!'+
' Expression: {}["__pro"+"to__"].foo = 1');
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');
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');
});
});
});
describe('overriding constructor', function() {
it('should evaluate grouped expressions', function() {
scope.foo = function foo() {
return "foo";
};
// When not overridden, access should be restricted both by the dot operator and by the
// index operator.
it('should prevent the exploit', function() {
expect(function() {
scope.$eval('foo.constructor()', scope);
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: foo.constructor()');
expect(function() {
scope.$eval('foo["constructor"]()', scope);
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: foo["constructor"]()');
// User defined value assigned to constructor.
scope.foo.constructor = function constructor() {
return "custom constructor";
};
// Dot operator should still block it.
expect(function() {
scope.$eval('foo.constructor()', scope);
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: foo.constructor()');
// However, the index operator should allow it.
expect(scope.$eval('foo["constructor"]()', scope)).toBe('custom constructor');
});
scope.$eval('' +
' "".sub.call.call(' +
'({})["constructor"].getOwnPropertyDescriptor("".sub.__proto__, "constructor").value,' +
'null,' +
'"alert(1)"' +
')()' +
'')
}).toThrow();
})
});
it('should call the function from the received instance and not from a new one', function() {