aboutsummaryrefslogtreecommitdiffstats
path: root/src/ng/parse.js
diff options
context:
space:
mode:
authorChirayu Krishnappa2013-06-24 14:14:54 -0700
committerIgor Minar2013-07-03 00:03:56 -0700
commit5349b20097dc5cdff0216ee219ac5f6e6ef8c219 (patch)
tree660ae167076f34018a1ff80565aab6ccdec1d8f4 /src/ng/parse.js
parentfd87eb0ca5e14f213d8b31280d444dbc29c20c50 (diff)
downloadangular.js-5349b20097dc5cdff0216ee219ac5f6e6ef8c219.tar.bz2
fix($parse): disallow access to Function constructor
Enhances sandboxing of Angular Expressions to prevent attacks via: {}.toString.constructor(alert("evil JS code"))
Diffstat (limited to 'src/ng/parse.js')
-rw-r--r--src/ng/parse.js87
1 files changed, 72 insertions, 15 deletions
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 28f69a60..102b3312 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -1,5 +1,54 @@
'use strict';
+var $parseMinErr = minErr('$parse');
+
+// 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.
+//
+// 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
+// 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.
+//
+// This sandboxing techniqueue 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.
+//
+// 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.
+
+function ensureSafeMemberName(name, fullExpression) {
+ if (name === "constructor") {
+ throw $parseMinErr('isecfld',
+ 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', fullExpression);
+ }
+ return name;
+};
+
+function ensureSafeObject(obj, fullExpression) {
+ // nifty check if obj is Function that is fast and works across iframes and other contexts
+ if (obj && obj.constructor === obj) {
+ throw $parseMinErr('isecfn',
+ 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression);
+ } else {
+ return obj;
+ }
+}
+
+
var OPERATORS = {
'null':function(){return null;},
'true':function(){return true;},
@@ -36,7 +85,6 @@ var OPERATORS = {
'!':function(self, locals, a){return !a(self, locals);}
};
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
-var $parseMinErr = minErr('$parse');
function lex(text, csp){
var tokens = [],
@@ -204,12 +252,12 @@ function lex(text, csp){
if (OPERATORS.hasOwnProperty(ident)) {
token.fn = token.json = OPERATORS[ident];
} else {
- var getter = getterFn(ident, csp);
+ var getter = getterFn(ident, csp, text);
token.fn = extend(function(self, locals) {
return (getter(self, locals));
}, {
assign: function(self, value) {
- return setter(self, ident, value);
+ return setter(self, ident, value, text);
}
});
}
@@ -583,14 +631,14 @@ function parser(text, json, $filter, csp){
function _fieldAccess(object) {
var field = expect().text;
- var getter = getterFn(field, csp);
+ var getter = getterFn(field, csp, text);
return extend(
function(scope, locals, self) {
return getter(self || object(scope, locals), locals);
},
{
assign:function(scope, value, locals) {
- return setter(object(scope, locals), field, value);
+ return setter(object(scope, locals), field, value, text);
}
}
);
@@ -606,7 +654,7 @@ function parser(text, json, $filter, csp){
v, p;
if (!o) return undefined;
- v = o[i];
+ v = ensureSafeObject(o[i], text);
if (v && v.then) {
p = v;
if (!('$$v' in v)) {
@@ -618,7 +666,9 @@ function parser(text, json, $filter, csp){
return v;
}, {
assign:function(self, value, locals){
- return obj(self, locals)[indexFn(self, locals)] = value;
+ var key = indexFn(self, locals);
+ // prevent overwriting of Function.constructor which would break ensureSafeObject check
+ return ensureSafeObject(obj(self, locals), text)[key] = value;
}
});
}
@@ -706,10 +756,10 @@ function parser(text, json, $filter, csp){
// Parser helper functions
//////////////////////////////////////////////////
-function setter(obj, path, setValue) {
- var element = path.split('.');
+function setter(obj, path, setValue, fullExp) {
+ var element = path.split('.'), key;
for (var i = 0; element.length > 1; i++) {
- var key = element.shift();
+ key = ensureSafeMemberName(element.shift(), fullExp);
var propertyObj = obj[key];
if (!propertyObj) {
propertyObj = {};
@@ -717,7 +767,8 @@ function setter(obj, path, setValue) {
}
obj = propertyObj;
}
- obj[element.shift()] = setValue;
+ key = ensureSafeMemberName(element.shift(), fullExp);
+ obj[key] = setValue;
return setValue;
}
@@ -728,7 +779,12 @@ var getterFnCache = {};
* - http://jsperf.com/angularjs-parse-getter/4
* - http://jsperf.com/path-evaluation-simplified/7
*/
-function cspSafeGetterFn(key0, key1, key2, key3, key4) {
+function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
+ ensureSafeMemberName(key0, fullExp);
+ ensureSafeMemberName(key1, fullExp);
+ ensureSafeMemberName(key2, fullExp);
+ ensureSafeMemberName(key3, fullExp);
+ ensureSafeMemberName(key4, fullExp);
return function(scope, locals) {
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
promise;
@@ -792,7 +848,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4) {
};
}
-function getterFn(path, csp) {
+function getterFn(path, csp, fullExp) {
if (getterFnCache.hasOwnProperty(path)) {
return getterFnCache[path];
}
@@ -803,12 +859,12 @@ function getterFn(path, csp) {
if (csp) {
fn = (pathKeysLength < 6)
- ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4])
+ ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp)
: function(scope, locals) {
var i = 0, val;
do {
val = cspSafeGetterFn(
- pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++]
+ pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp
)(scope, locals);
locals = undefined; // clear after first iteration
@@ -819,6 +875,7 @@ function getterFn(path, csp) {
} else {
var code = 'var l, fn, p;\n';
forEach(pathKeys, function(key, index) {
+ ensureSafeMemberName(key, fullExp);
code += 'if(s === null || s === undefined) return s;\n' +
'l=s;\n' +
's='+ (index