diff options
| author | Igor Minar | 2012-04-27 15:20:54 -0700 | 
|---|---|---|
| committer | Igor Minar | 2012-04-27 23:04:24 -0700 | 
| commit | 2b87c814ab70eaaff6359ce1a118f348c8bd2197 (patch) | |
| tree | 768d15a5b7b60f0560931763d7d093a4a571db35 /src/ng/parse.js | |
| parent | 2b1b2570344cfb55ba93b6f184bd3ee6db324419 (diff) | |
| download | angular.js-2b87c814ab70eaaff6359ce1a118f348c8bd2197.tar.bz2 | |
feat($parse): CSP compatibility
CSP (content security policy) forbids apps to use eval or
Function(string) generated functions (among other things). For us to be
compatible, we just need to implement the "getterFn" in $parse without
violating any of these restrictions.
We currently use Function(string) generated functions as a speed
optimization. With this change, it will be possible to opt into the CSP
compatible mode using the ngCsp directive. When this mode is on Angular
will evaluate all expressions up to 30% slower than in non-CSP mode, but
no security violations will be raised.
In order to use this feature put ngCsp directive on the root element of
the application. For example:
<!doctype html>
<html ng-app ng-csp>
  ...
  ...
</html>
Closes #893
Diffstat (limited to 'src/ng/parse.js')
| -rw-r--r-- | src/ng/parse.js | 145 | 
1 files changed, 116 insertions, 29 deletions
| diff --git a/src/ng/parse.js b/src/ng/parse.js index e5cb55d7..a367c291 100644 --- a/src/ng/parse.js +++ b/src/ng/parse.js @@ -27,7 +27,7 @@ var OPERATORS = {  };  var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; -function lex(text){ +function lex(text, csp){    var tokens = [],        token,        index = 0, @@ -187,7 +187,7 @@ function lex(text){      if (OPERATORS.hasOwnProperty(ident)) {        token.fn = token.json = OPERATORS[ident];      } else { -      var getter = getterFn(ident); +      var getter = getterFn(ident, csp);        token.fn = extend(function(self, locals) {          return (getter(self, locals));        }, { @@ -261,10 +261,10 @@ function lex(text){  ///////////////////////////////////////// -function parser(text, json, $filter){ +function parser(text, json, $filter, csp){    var ZERO = valueFn(0),        value, -      tokens = lex(text), +      tokens = lex(text, csp),        assignment = _assignment,        functionCall = _functionCall,        fieldAccess = _fieldAccess, @@ -532,7 +532,7 @@ function parser(text, json, $filter){    function _fieldAccess(object) {      var field = expect().text; -    var getter = getterFn(field); +    var getter = getterFn(field, csp);      return extend(          function(self, locals) {            return getter(object(self, locals), locals); @@ -685,32 +685,119 @@ function getter(obj, path, bindFnToScope) {  var getterFnCache = {}; -function getterFn(path) { +/** + * Implementation of the "Black Hole" variant from: + * - http://jsperf.com/angularjs-parse-getter/4 + * - http://jsperf.com/path-evaluation-simplified/7 + */ +function cspSafeGetterFn(key0, key1, key2, key3, key4) { +  return function(scope, locals) { +    var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, +        promise; + +    if (!pathVal) return pathVal; + +    pathVal = pathVal[key0]; +    if (pathVal && pathVal.then) { +      if (!("$$v" in pathVal)) { +        promise = pathVal; +        promise.$$v = undefined; +        promise.then(function(val) { promise.$$v = val; }); +      } +      pathVal = pathVal.$$v; +    } +    if (!key1 || !pathVal) return pathVal; + +    pathVal = pathVal[key1]; +    if (pathVal && pathVal.then) { +      if (!("$$v" in pathVal)) { +        promise = pathVal; +        promise.$$v = undefined; +        promise.then(function(val) { promise.$$v = val; }); +      } +      pathVal = pathVal.$$v; +    } +    if (!key2 || !pathVal) return pathVal; + +    pathVal = pathVal[key2]; +    if (pathVal && pathVal.then) { +      if (!("$$v" in pathVal)) { +        promise = pathVal; +        promise.$$v = undefined; +        promise.then(function(val) { promise.$$v = val; }); +      } +      pathVal = pathVal.$$v; +    } +    if (!key3 || !pathVal) return pathVal; + +    pathVal = pathVal[key3]; +    if (pathVal && pathVal.then) { +      if (!("$$v" in pathVal)) { +        promise = pathVal; +        promise.$$v = undefined; +        promise.then(function(val) { promise.$$v = val; }); +      } +      pathVal = pathVal.$$v; +    } +    if (!key4 || !pathVal) return pathVal; + +    pathVal = pathVal[key4]; +    if (pathVal && pathVal.then) { +      if (!("$$v" in pathVal)) { +        promise = pathVal; +        promise.$$v = undefined; +        promise.then(function(val) { promise.$$v = val; }); +      } +      pathVal = pathVal.$$v; +    } +    return pathVal; +  }; +}; + +function getterFn(path, csp) {    if (getterFnCache.hasOwnProperty(path)) {      return getterFnCache[path];    } -  var fn, code = 'var l, fn, p;\n'; -  forEach(path.split('.'), function(key, index) { -    code += 'if(!s) return s;\n' + -            'l=s;\n' + -            's='+ (index -                    // we simply direference 's' on any .dot notation -                    ? 's' -                    // but if we are first then we check locals firs, and if so read it first -                    : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + -            'if (s && s.then) {\n' + -              ' if (!("$$v" in s)) {\n' + -                ' p=s;\n' + -                ' p.$$v = undefined;\n' + -                ' p.then(function(v) {p.$$v=v;});\n' + -                '}\n' + -              ' s=s.$$v\n' + -            '}\n'; -  }); -  code += 'return s;'; -  fn = Function('s', 'k', code); -  fn.toString = function() { return code; }; +  var pathKeys = path.split('.'), +      pathKeysLength = pathKeys.length, +      fn; + +  if (csp) { +    fn = (pathKeysLength < 6) +        ? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4]) +        : function(scope, locals) { +          var i = 0, val; +          do { +            val = cspSafeGetterFn( +                    pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++] +                  )(scope, locals); +            locals = undefined; // clear after first iteration +          } while (i < pathKeysLength); +        }; +  } else { +    var code = 'var l, fn, p;\n'; +    forEach(pathKeys, function(key, index) { +      code += 'if(!s) return s;\n' + +              'l=s;\n' + +              's='+ (index +                      // we simply dereference 's' on any .dot notation +                      ? 's' +                      // but if we are first then we check locals first, and if so read it first +                      : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + +              'if (s && s.then) {\n' + +                ' if (!("$$v" in s)) {\n' + +                  ' p=s;\n' + +                  ' p.$$v = undefined;\n' + +                  ' p.then(function(v) {p.$$v=v;});\n' + +                  '}\n' + +                ' s=s.$$v\n' + +              '}\n'; +    }); +    code += 'return s;'; +    fn = Function('s', 'k', code); // s=scope, k=locals +    fn.toString = function() { return code; }; +  }    return getterFnCache[path] = fn;  } @@ -719,13 +806,13 @@ function getterFn(path) {  function $ParseProvider() {    var cache = {}; -  this.$get = ['$filter', function($filter) { +  this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {      return function(exp) {        switch(typeof exp) {          case 'string':            return cache.hasOwnProperty(exp)              ? cache[exp] -            : cache[exp] =  parser(exp, false, $filter); +            : cache[exp] =  parser(exp, false, $filter, $sniffer.csp);          case 'function':            return exp;          default: | 
