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

Commit e057a9a

Browse files
committedNov 7, 2014
fix($parse, events): prevent accidental misuse of properties on $event
1 parent e676d64 commit e057a9a

File tree

4 files changed

+78
-24
lines changed

4 files changed

+78
-24
lines changed
 

‎src/ng/directive/ngEventDirs.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ forEach(
5353
return {
5454
restrict: 'A',
5555
compile: function($element, attr) {
56-
var fn = $parse(attr[directiveName]);
56+
// We expose the powerful $event object on the scope that provides access to the Window,
57+
// etc. that isn't protected by the fast paths in $parse. We explicitly request better
58+
// checks at the cost of speed since event handler expressions are not executed as
59+
// frequently as regular change detection.
60+
var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
5761
return function ngEventHandler(scope, element) {
5862
element.on(eventName, function(event) {
5963
var callback = function() {

‎src/ng/parse.js

+33-20
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,8 @@ function setter(obj, path, setValue, fullExp) {
852852
return setValue;
853853
}
854854

855-
var getterFnCache = createMap();
855+
var getterFnCacheDefault = createMap();
856+
var getterFnCacheExpensive = createMap();
856857

857858
function isPossiblyDangerousMemberName(name) {
858859
return name == 'constructor';
@@ -863,7 +864,7 @@ function isPossiblyDangerousMemberName(name) {
863864
* - http://jsperf.com/angularjs-parse-getter/4
864865
* - http://jsperf.com/path-evaluation-simplified/7
865866
*/
866-
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
867+
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) {
867868
ensureSafeMemberName(key0, fullExp);
868869
ensureSafeMemberName(key1, fullExp);
869870
ensureSafeMemberName(key2, fullExp);
@@ -872,11 +873,11 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
872873
var eso = function(o) {
873874
return ensureSafeObject(o, fullExp);
874875
};
875-
var eso0 = isPossiblyDangerousMemberName(key0) ? eso : identity;
876-
var eso1 = isPossiblyDangerousMemberName(key1) ? eso : identity;
877-
var eso2 = isPossiblyDangerousMemberName(key2) ? eso : identity;
878-
var eso3 = isPossiblyDangerousMemberName(key3) ? eso : identity;
879-
var eso4 = isPossiblyDangerousMemberName(key4) ? eso : identity;
876+
var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity;
877+
var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity;
878+
var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity;
879+
var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity;
880+
var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity;
880881

881882
return function cspSafeGetter(scope, locals) {
882883
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
@@ -911,23 +912,25 @@ function getterFnWithEnsureSafeObject(fn, fullExpression) {
911912
}
912913

913914
function getterFn(path, options, fullExp) {
915+
var expensiveChecks = options.expensiveChecks;
916+
var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault);
914917
var fn = getterFnCache[path];
915-
916918
if (fn) return fn;
917919

920+
918921
var pathKeys = path.split('.'),
919922
pathKeysLength = pathKeys.length;
920923

921924
// http://jsperf.com/angularjs-parse-getter/6
922925
if (options.csp) {
923926
if (pathKeysLength < 6) {
924-
fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp);
927+
fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks);
925928
} else {
926929
fn = function cspSafeGetter(scope, locals) {
927930
var i = 0, val;
928931
do {
929932
val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
930-
pathKeys[i++], fullExp)(scope, locals);
933+
pathKeys[i++], fullExp, expensiveChecks)(scope, locals);
931934

932935
locals = undefined; // clear after first iteration
933936
scope = val;
@@ -937,15 +940,18 @@ function getterFn(path, options, fullExp) {
937940
}
938941
} else {
939942
var code = '';
940-
var needsEnsureSafeObject = false;
943+
if (expensiveChecks) {
944+
code += 's = eso(s, fe);\nl = eso(l, fe);\n';
945+
}
946+
var needsEnsureSafeObject = expensiveChecks;
941947
forEach(pathKeys, function(key, index) {
942948
ensureSafeMemberName(key, fullExp);
943949
var lookupJs = (index
944950
// we simply dereference 's' on any .dot notation
945951
? 's'
946952
// but if we are first then we check locals first, and if so read it first
947953
: '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key;
948-
if (isPossiblyDangerousMemberName(key)) {
954+
if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
949955
lookupJs = 'eso(' + lookupJs + ', fe)';
950956
needsEnsureSafeObject = true;
951957
}
@@ -1030,15 +1036,20 @@ function getValueOf(value) {
10301036
* service.
10311037
*/
10321038
function $ParseProvider() {
1033-
var cache = createMap();
1039+
var cacheDefault = createMap();
1040+
var cacheExpensive = createMap();
10341041

1035-
var $parseOptions = {
1036-
csp: false
1037-
};
10381042

10391043

10401044
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
1041-
$parseOptions.csp = $sniffer.csp;
1045+
var $parseOptions = {
1046+
csp: $sniffer.csp,
1047+
expensiveChecks: false
1048+
},
1049+
$parseOptionsExpensive = {
1050+
csp: $sniffer.csp,
1051+
expensiveChecks: true
1052+
};
10421053

10431054
function wrapSharedExpression(exp) {
10441055
var wrapped = exp;
@@ -1055,13 +1066,14 @@ function $ParseProvider() {
10551066
return wrapped;
10561067
}
10571068

1058-
return function $parse(exp, interceptorFn) {
1069+
return function $parse(exp, interceptorFn, expensiveChecks) {
10591070
var parsedExpression, oneTime, cacheKey;
10601071

10611072
switch (typeof exp) {
10621073
case 'string':
10631074
cacheKey = exp = exp.trim();
10641075

1076+
var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
10651077
parsedExpression = cache[cacheKey];
10661078

10671079
if (!parsedExpression) {
@@ -1070,8 +1082,9 @@ function $ParseProvider() {
10701082
exp = exp.substring(2);
10711083
}
10721084

1073-
var lexer = new Lexer($parseOptions);
1074-
var parser = new Parser(lexer, $filter, $parseOptions);
1085+
var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
1086+
var lexer = new Lexer(parseOptions);
1087+
var parser = new Parser(lexer, $filter, parseOptions);
10751088
parsedExpression = parser.parse(exp);
10761089

10771090
if (parsedExpression.constant) {

‎test/ng/directive/ngEventDirsSpec.js

+19
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ describe('event directives', function() {
9090

9191
});
9292

93+
describe('security', function() {
94+
it('should allow access to the $event object', inject(function($rootScope, $compile) {
95+
var scope = $rootScope.$new();
96+
element = $compile('<button ng-click="e = $event">BTN</button>')(scope);
97+
element.triggerHandler('click');
98+
expect(scope.e.target).toBe(element[0]);
99+
}));
100+
101+
it('should block access to DOM nodes (e.g. exposed via $event)', inject(function($rootScope, $compile) {
102+
var scope = $rootScope.$new();
103+
element = $compile('<button ng-click="e = $event.target">BTN</button>')(scope);
104+
expect(function() {
105+
element.triggerHandler('click');
106+
}).toThrowMinErr(
107+
'$parse', 'isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! ' +
108+
'Expression: e = $event.target');
109+
}));
110+
});
111+
93112
describe('blur', function() {
94113

95114
describe('call the listener asynchronously during $apply', function() {

‎test/ng/parseSpec.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
describe('parser', function() {
44

55
beforeEach(function() {
6-
/* global getterFnCache: true */
7-
// clear cache
8-
getterFnCache = createMap();
6+
/* global getterFnCacheDefault: true */
7+
/* global getterFnCacheExpensive: true */
8+
// clear caches
9+
getterFnCacheDefault = createMap();
10+
getterFnCacheExpensive = createMap();
911
});
1012

1113

@@ -783,6 +785,22 @@ describe('parser', function() {
783785
'Expression: foo["bar"]');
784786

785787
});
788+
789+
describe('expensiveChecks', function() {
790+
it('should block access to window object even when aliased', inject(function($parse, $window) {
791+
scope.foo = {w: $window};
792+
// This isn't blocked for performance.
793+
expect(scope.$eval($parse('foo.w'))).toBe($window);
794+
// Event handlers use the more expensive path for better protection since they expose
795+
// the $event object on the scope.
796+
expect(function() {
797+
scope.$eval($parse('foo.w', null, true));
798+
}).toThrowMinErr(
799+
'$parse', 'isecwindow', 'Referencing the Window in Angular expressions is disallowed! ' +
800+
'Expression: foo.w');
801+
802+
}));
803+
});
786804
});
787805

788806
describe('Function prototype functions', function() {

2 commit comments

Comments
 (2)

gkalpak commented on Nov 7, 2014

@gkalpak
Member

I wonder if this should be documented as a breaking change.
I guess we neverwanted people to access DOM nodes or Window objects in the view, but I don't think it was mentioned anywhere as impossible.

caitp commented on Nov 7, 2014

@caitp
Contributor

it should be documented, because every time we change this and add new restrictions people file bugs and get mad

This repository has been archived.