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

Commit 5349b20

Browse files
chirayukIgorMinar
authored andcommitted
fix($parse): disallow access to Function constructor
Enhances sandboxing of Angular Expressions to prevent attacks via: {}.toString.constructor(alert("evil JS code"))
1 parent fd87eb0 commit 5349b20

File tree

2 files changed

+216
-29
lines changed

2 files changed

+216
-29
lines changed

src/ng/parse.js

+72-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,54 @@
11
'use strict';
22

3+
var $parseMinErr = minErr('$parse');
4+
5+
// Sandboxing Angular Expressions
6+
// ------------------------------
7+
// Angular expressions are generally considered safe because these expressions only have direct access to $scope and
8+
// locals. However, one can obtain the ability to execute arbitrary JS code by obtaining a reference to native JS
9+
// functions such as the Function constructor.
10+
//
11+
// As an example, consider the following Angular expression:
12+
//
13+
// {}.toString.constructor(alert("evil JS code"))
14+
//
15+
// We want to prevent this type of access. For the sake of performance, during the lexing phase we disallow any "dotted"
16+
// access to any member named "constructor".
17+
//
18+
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor while evaluating
19+
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor while evaluating
20+
// the expression, which is a stronger but more expensive test. Since reflective calls are expensive anyway, this is not
21+
// such a big deal compared to static dereferencing.
22+
//
23+
// This sandboxing techniqueue is not perfect and doesn't aim to be. The goal is to prevent exploits against the
24+
// expression language, but not to prevent exploits that were enabled by exposing sensitive JavaScript or browser apis
25+
// on Scope. Exposing such objects on a Scope is never a good practice and therefore we are not even trying to protect
26+
// against interaction with an object explicitly exposed in this way.
27+
//
28+
// A developer could foil the name check by aliasing the Function constructor under a different name on the scope.
29+
//
30+
// In general, it is not possible to access a Window object from an angular expression unless a window or some DOM
31+
// object that has a reference to window is published onto a Scope.
32+
33+
function ensureSafeMemberName(name, fullExpression) {
34+
if (name === "constructor") {
35+
throw $parseMinErr('isecfld',
36+
'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', fullExpression);
37+
}
38+
return name;
39+
};
40+
41+
function ensureSafeObject(obj, fullExpression) {
42+
// nifty check if obj is Function that is fast and works across iframes and other contexts
43+
if (obj && obj.constructor === obj) {
44+
throw $parseMinErr('isecfn',
45+
'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression);
46+
} else {
47+
return obj;
48+
}
49+
}
50+
51+
352
var OPERATORS = {
453
'null':function(){return null;},
554
'true':function(){return true;},
@@ -36,7 +85,6 @@ var OPERATORS = {
3685
'!':function(self, locals, a){return !a(self, locals);}
3786
};
3887
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
39-
var $parseMinErr = minErr('$parse');
4088

4189
function lex(text, csp){
4290
var tokens = [],
@@ -204,12 +252,12 @@ function lex(text, csp){
204252
if (OPERATORS.hasOwnProperty(ident)) {
205253
token.fn = token.json = OPERATORS[ident];
206254
} else {
207-
var getter = getterFn(ident, csp);
255+
var getter = getterFn(ident, csp, text);
208256
token.fn = extend(function(self, locals) {
209257
return (getter(self, locals));
210258
}, {
211259
assign: function(self, value) {
212-
return setter(self, ident, value);
260+
return setter(self, ident, value, text);
213261
}
214262
});
215263
}
@@ -583,14 +631,14 @@ function parser(text, json, $filter, csp){
583631

584632
function _fieldAccess(object) {
585633
var field = expect().text;
586-
var getter = getterFn(field, csp);
634+
var getter = getterFn(field, csp, text);
587635
return extend(
588636
function(scope, locals, self) {
589637
return getter(self || object(scope, locals), locals);
590638
},
591639
{
592640
assign:function(scope, value, locals) {
593-
return setter(object(scope, locals), field, value);
641+
return setter(object(scope, locals), field, value, text);
594642
}
595643
}
596644
);
@@ -606,7 +654,7 @@ function parser(text, json, $filter, csp){
606654
v, p;
607655

608656
if (!o) return undefined;
609-
v = o[i];
657+
v = ensureSafeObject(o[i], text);
610658
if (v && v.then) {
611659
p = v;
612660
if (!('$$v' in v)) {
@@ -618,7 +666,9 @@ function parser(text, json, $filter, csp){
618666
return v;
619667
}, {
620668
assign:function(self, value, locals){
621-
return obj(self, locals)[indexFn(self, locals)] = value;
669+
var key = indexFn(self, locals);
670+
// prevent overwriting of Function.constructor which would break ensureSafeObject check
671+
return ensureSafeObject(obj(self, locals), text)[key] = value;
622672
}
623673
});
624674
}
@@ -706,18 +756,19 @@ function parser(text, json, $filter, csp){
706756
// Parser helper functions
707757
//////////////////////////////////////////////////
708758

709-
function setter(obj, path, setValue) {
710-
var element = path.split('.');
759+
function setter(obj, path, setValue, fullExp) {
760+
var element = path.split('.'), key;
711761
for (var i = 0; element.length > 1; i++) {
712-
var key = element.shift();
762+
key = ensureSafeMemberName(element.shift(), fullExp);
713763
var propertyObj = obj[key];
714764
if (!propertyObj) {
715765
propertyObj = {};
716766
obj[key] = propertyObj;
717767
}
718768
obj = propertyObj;
719769
}
720-
obj[element.shift()] = setValue;
770+
key = ensureSafeMemberName(element.shift(), fullExp);
771+
obj[key] = setValue;
721772
return setValue;
722773
}
723774

@@ -728,7 +779,12 @@ var getterFnCache = {};
728779
* - http://jsperf.com/angularjs-parse-getter/4
729780
* - http://jsperf.com/path-evaluation-simplified/7
730781
*/
731-
function cspSafeGetterFn(key0, key1, key2, key3, key4) {
782+
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
783+
ensureSafeMemberName(key0, fullExp);
784+
ensureSafeMemberName(key1, fullExp);
785+
ensureSafeMemberName(key2, fullExp);
786+
ensureSafeMemberName(key3, fullExp);
787+
ensureSafeMemberName(key4, fullExp);
732788
return function(scope, locals) {
733789
var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
734790
promise;
@@ -792,7 +848,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4) {
792848
};
793849
}
794850

795-
function getterFn(path, csp) {
851+
function getterFn(path, csp, fullExp) {
796852
if (getterFnCache.hasOwnProperty(path)) {
797853
return getterFnCache[path];
798854
}
@@ -803,12 +859,12 @@ function getterFn(path, csp) {
803859

804860
if (csp) {
805861
fn = (pathKeysLength < 6)
806-
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4])
862+
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp)
807863
: function(scope, locals) {
808864
var i = 0, val;
809865
do {
810866
val = cspSafeGetterFn(
811-
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++]
867+
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp
812868
)(scope, locals);
813869

814870
locals = undefined; // clear after first iteration
@@ -819,6 +875,7 @@ function getterFn(path, csp) {
819875
} else {
820876
var code = 'var l, fn, p;\n';
821877
forEach(pathKeys, function(key, index) {
878+
ensureSafeMemberName(key, fullExp);
822879
code += 'if(s === null || s === undefined) return s;\n' +
823880
'l=s;\n' +
824881
's='+ (index

0 commit comments

Comments
 (0)