Skip to content

Commit

Permalink
feat($parse): revert hiding "private" properties
Browse files Browse the repository at this point in the history
Hiding `_*` properties was a feature primarily for developers using Closure compiler and Google JS
style. We didn't realize how many people will be affected by this change.

We might introduce this feature in the future, probably under a config option, but it needs more
research and so I'm reverting the change for now.

This reverts commit 3d6a89e.

Closes angular#4926
Closes angular#4842
Closes angular#4865
Closes angular#4859
Closes angular#4849

Conflicts:
	src/ng/parse.js
  • Loading branch information
vojtajina authored and jamesdaily committed Jan 27, 2014
1 parent 8f06373 commit 8d4f667
Show file tree
Hide file tree
Showing 3 changed files with 11 additions and 140 deletions.
50 changes: 0 additions & 50 deletions docs/content/error/parse/isecprv.ngdoc

This file was deleted.

34 changes: 9 additions & 25 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,18 @@ var promiseWarning;
// ------------------------------
// Angular expressions are generally considered safe because these expressions only have direct
// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by
// obtaining a reference to native JS functions such as the Function constructor, the global Window
// or Document object. In addition, many powerful functions for use by JavaScript code are
// published on scope that shouldn't be available from within an Angular expression.
// obtaining a reference to native JS functions such as the Function constructor.
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor(alert("evil JS code"))
//
// We want to prevent this type of access. For the sake of performance, during the lexing phase we
// disallow any "dotted" access to any member named "constructor" or to any member whose name begins
// or ends with an underscore. The latter allows one to exclude the private / JavaScript only API
// available on the scope and controllers from the context of an Angular expression.
// disallow any "dotted" access to any member named "constructor".
//
// For reflective calls (a[b]), we check that the value of the lookup is not the Function
// constructor, Window or DOM node while evaluating the expression, which is a stronger but more
// expensive test. Since reflective calls are expensive anyway, this is not such a big deal compared
// to static dereferencing.
// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor
// while evaluating the expression, which is a stronger but more expensive test. Since reflective
// calls are expensive anyway, this is not such a big deal compared to static dereferencing.
//
// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
// against the expression language, but not to prevent exploits that were enabled by exposing
Expand All @@ -38,20 +33,12 @@ var promiseWarning;
// In general, it is not possible to access a Window object from an angular expression unless a
// window or some DOM object that has a reference to window is published onto a Scope.

function ensureSafeMemberName(name, fullExpression, allowConstructor) {
if (typeof name !== 'string' && toString.apply(name) !== "[object String]") {
return name;
}
if (name === "constructor" && !allowConstructor) {
function ensureSafeMemberName(name, fullExpression) {
if (name === "constructor") {
throw $parseMinErr('isecfld',
'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}',
fullExpression);
}
if (name.charAt(0) === '_' || name.charAt(name.length-1) === '_') {
throw $parseMinErr('isecprv',
'Referencing private fields in Angular expressions is disallowed! Expression: {0}',
fullExpression);
}
return name;
}

Expand Down Expand Up @@ -735,10 +722,7 @@ Parser.prototype = {

return extend(function(self, locals) {
var o = obj(self, locals),
// In the getter, we will not block looking up "constructor" by name in order to support user defined
// constructors. However, if value looked up is the Function constructor, we will still block it in the
// ensureSafeObject call right after we look up o[i] (a few lines below.)
i = ensureSafeMemberName(indexFn(self, locals), parser.text, true /* allowConstructor */),
i = indexFn(self, locals),
v, p;

if (!o) return undefined;
Expand All @@ -754,7 +738,7 @@ Parser.prototype = {
return v;
}, {
assign: function(self, value, locals) {
var key = ensureSafeMemberName(indexFn(self, locals), parser.text);
var key = indexFn(self, locals);
// prevent overwriting of Function.constructor which would break ensureSafeObject check
var safe = ensureSafeObject(obj(self, locals), parser.text);
return safe[key] = value;
Expand Down
67 changes: 2 additions & 65 deletions test/ng/parseSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,57 +591,6 @@ describe('parser', function() {
});

describe('sandboxing', function() {
describe('private members', function() {
it('should NOT allow access to private members', function() {
forEach(['_name', 'name_', '_', '_name_'], function(name) {
function _testExpression(expression) {
scope.a = {b: name};
scope[name] = {a: scope.a};
scope.piece_1 = "XX" + name.charAt(0) + "XX";
scope.piece_2 = "XX" + name.substr(1) + "XX";
expect(function() {
scope.$eval(expression);
}).toThrowMinErr(
'$parse', 'isecprv', 'Referencing private fields in Angular expressions is disallowed! ' +
'Expression: ' + expression);
}

function testExpression(expression) {
if (expression.indexOf('"NAME"') != -1) {
var concatExpr = 'piece_1.substr(2, 1) + piece_2.substr(2, LEN)'.replace('LEN', name.length-1);
_testExpression(expression.replace(/"NAME"/g, concatExpr));
_testExpression(expression.replace(/"NAME"/g, '(' + concatExpr + ')'));
}
_testExpression(expression.replace(/NAME/g, name));
}

// Not all of these are exploitable. The tests ensure that the contract is honored
// without caring about the implementation or exploitability.
testExpression('NAME'); testExpression('NAME = 1');
testExpression('(NAME)'); testExpression('(NAME) = 1');
testExpression('a.NAME'); testExpression('a.NAME = 1');
testExpression('NAME.b'); testExpression('NAME.b = 1');
testExpression('a.NAME.b'); testExpression('a.NAME.b = 1');
testExpression('NAME()'); testExpression('NAME() = 1');
testExpression('(NAME)()'); testExpression('(NAME = 1)()');
testExpression('(NAME).foo()'); testExpression('(NAME = 1).foo()');
testExpression('a.NAME()'); testExpression('a.NAME() = 1');
testExpression('a.NAME.foo()'); testExpression('a.NAME.foo()');
testExpression('foo(NAME)'); testExpression('foo(NAME = 1)');
testExpression('foo(a.NAME)'); testExpression('foo(a.NAME = 1)');
testExpression('foo(1, a.NAME)'); testExpression('foo(1, a.NAME = 1)');
testExpression('foo(a["NAME"])'); testExpression('foo(a["NAME"] = 1)');
testExpression('foo(1, a["NAME"])'); testExpression('foo(1, a["NAME"] = 1)');
testExpression('foo(b = a["NAME"])'); testExpression('foo(b = (a["NAME"] = 1))');
testExpression('a["NAME"]'); testExpression('a["NAME"] = 1');
testExpression('a["NAME"]()');
testExpression('a["NAME"].foo()');
testExpression('a.b["NAME"]'); testExpression('a.b["NAME"] = 1');
testExpression('a["b"]["NAME"]'); testExpression('a["b"]["NAME"] = 1');
});
});
});

describe('Function constructor', function() {
it('should NOT allow access to Function constructor in getter', function() {
expect(function() {
Expand Down Expand Up @@ -702,29 +651,17 @@ describe('parser', function() {
expect(function() {
scope.$eval('{}.toString["constructor"]["constructor"] = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString["constructor"]["constructor"] = 1');


scope.key1 = "const";
scope.key2 = "ructor";
expect(function() {
scope.$eval('{}.toString[key1 + key2].foo');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString[key1 + key2].foo');

expect(function() {
scope.$eval('{}.toString[key1 + key2] = 1');
}).toThrowMinErr(
'$parse', 'isecfld', 'Referencing "constructor" field in Angular expressions is disallowed! ' +
'Expression: {}.toString[key1 + key2] = 1');

expect(function() {
scope.$eval('{}.toString[key1 + key2].foo = 1');
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: {}.toString[key1 + key2].foo = 1');
'Expression: {}.toString[key1 + key2].foo = 1');

expect(function() {
scope.$eval('{}.toString["constructor"]["a"] = 1');
Expand Down

0 comments on commit 8d4f667

Please sign in to comment.