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

Commit 2b87c81

Browse files
committed
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
1 parent 2b1b257 commit 2b87c81

File tree

7 files changed

+544
-397
lines changed

7 files changed

+544
-397
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ angularFiles = {
4545
'src/ng/directive/ngClass.js',
4646
'src/ng/directive/ngCloak.js',
4747
'src/ng/directive/ngController.js',
48+
'src/ng/directive/ngCsp.js',
4849
'src/ng/directive/ngEventDirs.js',
4950
'src/ng/directive/ngInclude.js',
5051
'src/ng/directive/ngInit.js',

src/AngularPublic.js

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function publishExternalAPI(angular){
7676
ngClass: ngClassDirective,
7777
ngClassEven: ngClassEvenDirective,
7878
ngClassOdd: ngClassOddDirective,
79+
ngCsp: ngCspDirective,
7980
ngCloak: ngCloakDirective,
8081
ngController: ngControllerDirective,
8182
ngForm: ngFormDirective,

src/ng/directive/ngCsp.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
/**
4+
* TODO(i): this directive is not publicly documented until we know for sure that CSP can't be
5+
* safely feature-detected.
6+
*
7+
* @name angular.module.ng.$compileProvider.directive.ngCsp
8+
* @priority 1000
9+
*
10+
* @description
11+
* Enables CSP (Content Security Protection) support. This directive should be used on the `<html>`
12+
* element before any kind of interpolation or expression is processed.
13+
*
14+
* If enabled the performance of $parse will suffer.
15+
*
16+
* @element html
17+
*/
18+
19+
var ngCspDirective = ['$sniffer', function($sniffer) {
20+
return {
21+
priority: 1000,
22+
compile: function() {
23+
$sniffer.csp = true;
24+
}
25+
};
26+
}];

src/ng/parse.js

+116-29
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ var OPERATORS = {
2727
};
2828
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
2929

30-
function lex(text){
30+
function lex(text, csp){
3131
var tokens = [],
3232
token,
3333
index = 0,
@@ -187,7 +187,7 @@ function lex(text){
187187
if (OPERATORS.hasOwnProperty(ident)) {
188188
token.fn = token.json = OPERATORS[ident];
189189
} else {
190-
var getter = getterFn(ident);
190+
var getter = getterFn(ident, csp);
191191
token.fn = extend(function(self, locals) {
192192
return (getter(self, locals));
193193
}, {
@@ -261,10 +261,10 @@ function lex(text){
261261

262262
/////////////////////////////////////////
263263

264-
function parser(text, json, $filter){
264+
function parser(text, json, $filter, csp){
265265
var ZERO = valueFn(0),
266266
value,
267-
tokens = lex(text),
267+
tokens = lex(text, csp),
268268
assignment = _assignment,
269269
functionCall = _functionCall,
270270
fieldAccess = _fieldAccess,
@@ -532,7 +532,7 @@ function parser(text, json, $filter){
532532

533533
function _fieldAccess(object) {
534534
var field = expect().text;
535-
var getter = getterFn(field);
535+
var getter = getterFn(field, csp);
536536
return extend(
537537
function(self, locals) {
538538
return getter(object(self, locals), locals);
@@ -685,32 +685,119 @@ function getter(obj, path, bindFnToScope) {
685685

686686
var getterFnCache = {};
687687

688-
function getterFn(path) {
688+
/**
689+
* Implementation of the "Black Hole" variant from:
690+
* - http://jsperf.com/angularjs-parse-getter/4
691+
* - http://jsperf.com/path-evaluation-simplified/7
692+
*/
693+
function cspSafeGetterFn(key0, key1, key2, key3, key4) {
694+
return function(scope, locals) {
695+
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
696+
promise;
697+
698+
if (!pathVal) return pathVal;
699+
700+
pathVal = pathVal[key0];
701+
if (pathVal && pathVal.then) {
702+
if (!("$$v" in pathVal)) {
703+
promise = pathVal;
704+
promise.$$v = undefined;
705+
promise.then(function(val) { promise.$$v = val; });
706+
}
707+
pathVal = pathVal.$$v;
708+
}
709+
if (!key1 || !pathVal) return pathVal;
710+
711+
pathVal = pathVal[key1];
712+
if (pathVal && pathVal.then) {
713+
if (!("$$v" in pathVal)) {
714+
promise = pathVal;
715+
promise.$$v = undefined;
716+
promise.then(function(val) { promise.$$v = val; });
717+
}
718+
pathVal = pathVal.$$v;
719+
}
720+
if (!key2 || !pathVal) return pathVal;
721+
722+
pathVal = pathVal[key2];
723+
if (pathVal && pathVal.then) {
724+
if (!("$$v" in pathVal)) {
725+
promise = pathVal;
726+
promise.$$v = undefined;
727+
promise.then(function(val) { promise.$$v = val; });
728+
}
729+
pathVal = pathVal.$$v;
730+
}
731+
if (!key3 || !pathVal) return pathVal;
732+
733+
pathVal = pathVal[key3];
734+
if (pathVal && pathVal.then) {
735+
if (!("$$v" in pathVal)) {
736+
promise = pathVal;
737+
promise.$$v = undefined;
738+
promise.then(function(val) { promise.$$v = val; });
739+
}
740+
pathVal = pathVal.$$v;
741+
}
742+
if (!key4 || !pathVal) return pathVal;
743+
744+
pathVal = pathVal[key4];
745+
if (pathVal && pathVal.then) {
746+
if (!("$$v" in pathVal)) {
747+
promise = pathVal;
748+
promise.$$v = undefined;
749+
promise.then(function(val) { promise.$$v = val; });
750+
}
751+
pathVal = pathVal.$$v;
752+
}
753+
return pathVal;
754+
};
755+
};
756+
757+
function getterFn(path, csp) {
689758
if (getterFnCache.hasOwnProperty(path)) {
690759
return getterFnCache[path];
691760
}
692761

693-
var fn, code = 'var l, fn, p;\n';
694-
forEach(path.split('.'), function(key, index) {
695-
code += 'if(!s) return s;\n' +
696-
'l=s;\n' +
697-
's='+ (index
698-
// we simply direference 's' on any .dot notation
699-
? 's'
700-
// but if we are first then we check locals firs, and if so read it first
701-
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
702-
'if (s && s.then) {\n' +
703-
' if (!("$$v" in s)) {\n' +
704-
' p=s;\n' +
705-
' p.$$v = undefined;\n' +
706-
' p.then(function(v) {p.$$v=v;});\n' +
707-
'}\n' +
708-
' s=s.$$v\n' +
709-
'}\n';
710-
});
711-
code += 'return s;';
712-
fn = Function('s', 'k', code);
713-
fn.toString = function() { return code; };
762+
var pathKeys = path.split('.'),
763+
pathKeysLength = pathKeys.length,
764+
fn;
765+
766+
if (csp) {
767+
fn = (pathKeysLength < 6)
768+
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4])
769+
: function(scope, locals) {
770+
var i = 0, val;
771+
do {
772+
val = cspSafeGetterFn(
773+
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++]
774+
)(scope, locals);
775+
locals = undefined; // clear after first iteration
776+
} while (i < pathKeysLength);
777+
};
778+
} else {
779+
var code = 'var l, fn, p;\n';
780+
forEach(pathKeys, function(key, index) {
781+
code += 'if(!s) return s;\n' +
782+
'l=s;\n' +
783+
's='+ (index
784+
// we simply dereference 's' on any .dot notation
785+
? 's'
786+
// but if we are first then we check locals first, and if so read it first
787+
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
788+
'if (s && s.then) {\n' +
789+
' if (!("$$v" in s)) {\n' +
790+
' p=s;\n' +
791+
' p.$$v = undefined;\n' +
792+
' p.then(function(v) {p.$$v=v;});\n' +
793+
'}\n' +
794+
' s=s.$$v\n' +
795+
'}\n';
796+
});
797+
code += 'return s;';
798+
fn = Function('s', 'k', code); // s=scope, k=locals
799+
fn.toString = function() { return code; };
800+
}
714801

715802
return getterFnCache[path] = fn;
716803
}
@@ -719,13 +806,13 @@ function getterFn(path) {
719806

720807
function $ParseProvider() {
721808
var cache = {};
722-
this.$get = ['$filter', function($filter) {
809+
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
723810
return function(exp) {
724811
switch(typeof exp) {
725812
case 'string':
726813
return cache.hasOwnProperty(exp)
727814
? cache[exp]
728-
: cache[exp] = parser(exp, false, $filter);
815+
: cache[exp] = parser(exp, false, $filter, $sniffer.csp);
729816
case 'function':
730817
return exp;
731818
default:

src/ng/sniffer.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ function $SnifferProvider() {
2828
}
2929

3030
return eventSupport[event];
31-
}
31+
},
32+
// TODO(i): currently there is no way to feature detect CSP without triggering alerts
33+
csp: false
3234
};
3335
}];
3436
}

test/ng/directive/ngCspSpec.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
describe('ngCsp', function() {
4+
5+
it('it should turn on CSP mode in $sniffer', inject(function($sniffer, $compile) {
6+
expect($sniffer.csp).toBe(false);
7+
$compile('<div ng-csp></div>');
8+
expect($sniffer.csp).toBe(true);
9+
}));
10+
});

0 commit comments

Comments
 (0)