Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($parse): CSP compatibility
Browse files Browse the repository at this point in the history
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
  • Loading branch information
IgorMinar committed Apr 28, 2012
1 parent 2b1b257 commit 2b87c81
Show file tree
Hide file tree
Showing 7 changed files with 544 additions and 397 deletions.
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ angularFiles = {
'src/ng/directive/ngClass.js',
'src/ng/directive/ngCloak.js',
'src/ng/directive/ngController.js',
'src/ng/directive/ngCsp.js',
'src/ng/directive/ngEventDirs.js',
'src/ng/directive/ngInclude.js',
'src/ng/directive/ngInit.js',
Expand Down
1 change: 1 addition & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function publishExternalAPI(angular){
ngClass: ngClassDirective,
ngClassEven: ngClassEvenDirective,
ngClassOdd: ngClassOddDirective,
ngCsp: ngCspDirective,
ngCloak: ngCloakDirective,
ngController: ngControllerDirective,
ngForm: ngFormDirective,
Expand Down
26 changes: 26 additions & 0 deletions src/ng/directive/ngCsp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

/**
* TODO(i): this directive is not publicly documented until we know for sure that CSP can't be
* safely feature-detected.
*
* @name angular.module.ng.$compileProvider.directive.ngCsp
* @priority 1000
*
* @description
* Enables CSP (Content Security Protection) support. This directive should be used on the `<html>`
* element before any kind of interpolation or expression is processed.
*
* If enabled the performance of $parse will suffer.
*
* @element html
*/

var ngCspDirective = ['$sniffer', function($sniffer) {
return {
priority: 1000,
compile: function() {
$sniffer.csp = true;
}
};
}];
145 changes: 116 additions & 29 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}, {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/ng/sniffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ function $SnifferProvider() {
}

return eventSupport[event];
}
},
// TODO(i): currently there is no way to feature detect CSP without triggering alerts
csp: false
};
}];
}
10 changes: 10 additions & 0 deletions test/ng/directive/ngCspSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

describe('ngCsp', function() {

it('it should turn on CSP mode in $sniffer', inject(function($sniffer, $compile) {
expect($sniffer.csp).toBe(false);
$compile('<div ng-csp></div>');
expect($sniffer.csp).toBe(true);
}));
});
Loading

0 comments on commit 2b87c81

Please sign in to comment.