Skip to content

Commit c10f123

Browse files
petebacondarwinjamesdaily
authored andcommitted
fix(*): protect calls to hasOwnProperty in public API
Objects received from outside AngularJS may have had their `hasOwnProperty` method overridden with something else. In cases where we can do this without incurring a performance penalty we call directly on Object.prototype.hasOwnProperty to ensure that we use the correct method. Also, we have some internal hash objects, where the keys for the map are provided from outside AngularJS. In such cases we either prevent `hasOwnProperty` from being used as a key or provide some other way of preventing our objects from having their `hasOwnProperty` overridden. BREAKING CHANGE: Inputs with name equal to "hasOwnProperty" are not allowed inside form or ngForm directives. Before, inputs whose name was "hasOwnProperty" were quietly ignored and not added to the scope. Now a badname exception is thrown. Using "hasOwnProperty" for an input name would be very unusual and bad practice. Either do not include such an input in a `form` or `ngForm` directive or change the name of the input. Closes angular#3331
1 parent ba10a71 commit c10f123

24 files changed

+196
-17
lines changed

docs/content/error/ng/badname.ngdoc

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@ngdoc error
2+
@name ng:badname
3+
@fullName Bad `hasOwnProperty` Name
4+
@description
5+
6+
Occurs when you try to use the name `hasOwnProperty` in a context where it is not allow.
7+
Generally, a name cannot be `hasOwnProperty` because it is used, internally, on a object
8+
and allowing such a name would break lookups on this object.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@ngdoc error
2+
@name $resource:badname
3+
@fullName Cannot use hasOwnProperty as a parameter name
4+
@description
5+
6+
Occurs when you try to use the name `hasOwnProperty` as a name of a parameter.
7+
Generally, a name cannot be `hasOwnProperty` because it is used, internally, on a object
8+
and allowing such a name would break lookups on this object.

src/Angular.js

+13-10
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@
22

33
////////////////////////////////////
44

5-
/**
6-
* hasOwnProperty may be overwritten by a property of the same name, or entirely
7-
* absent from an object that does not inherit Object.prototype; this copy is
8-
* used instead
9-
*/
10-
var hasOwnPropertyFn = Object.prototype.hasOwnProperty;
11-
var hasOwnPropertyLocal = function(obj, key) {
12-
return hasOwnPropertyFn.call(obj, key);
13-
};
14-
155
/**
166
* @ngdoc function
177
* @name angular.lowercase
@@ -691,6 +681,8 @@ function shallowCopy(src, dst) {
691681
dst = dst || {};
692682

693683
for(var key in src) {
684+
// shallowCopy is only ever called by $compile nodeLinkFn, which has control over src
685+
// so we don't need to worry hasOwnProperty here
694686
if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') {
695687
dst[key] = src[key];
696688
}
@@ -1187,6 +1179,17 @@ function assertArgFn(arg, name, acceptArrayAnnotation) {
11871179
return arg;
11881180
}
11891181

1182+
/**
1183+
* throw error if the name given is hasOwnProperty
1184+
* @param {String} name the name to test
1185+
* @param {String} context the context in which the name is used, such as module or directive
1186+
*/
1187+
function assertNotHasOwnProperty(name, context) {
1188+
if (name === 'hasOwnProperty') {
1189+
throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context);
1190+
}
1191+
}
1192+
11901193
/**
11911194
* Return the value accessible from the object by path. Any undefined traversals are ignored
11921195
* @param {Object} obj starting object

src/auto/injector.js

+2
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ function createInjector(modulesToLoad) {
455455
}
456456

457457
function provider(name, provider_) {
458+
assertNotHasOwnProperty(name, 'service');
458459
if (isFunction(provider_) || isArray(provider_)) {
459460
provider_ = providerInjector.instantiate(provider_);
460461
}
@@ -475,6 +476,7 @@ function createInjector(modulesToLoad) {
475476
function value(name, value) { return factory(name, valueFn(value)); }
476477

477478
function constant(name, value) {
479+
assertNotHasOwnProperty(name, 'constant');
478480
providerCache[name] = value;
479481
instanceCache[name] = value;
480482
}

src/loader.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
function setupModuleLoader(window) {
1212

13+
var $injectorMinErr = minErr('$injector');
14+
1315
function ensure(obj, name, factory) {
1416
return obj[name] || (obj[name] = factory());
1517
}
@@ -68,12 +70,13 @@ function setupModuleLoader(window) {
6870
* @returns {module} new module with the {@link angular.Module} api.
6971
*/
7072
return function module(name, requires, configFn) {
73+
assertNotHasOwnProperty(name, 'module');
7174
if (requires && modules.hasOwnProperty(name)) {
7275
modules[name] = null;
7376
}
7477
return ensure(modules, name, function() {
7578
if (!requires) {
76-
throw minErr('$injector')('nomod', "Module '{0}' is not available! You either misspelled the module name " +
79+
throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled the module name " +
7780
"or forgot to load it. If registering a module ensure that you specify the dependencies as the second " +
7881
"argument.", name);
7982
}

src/ng/compile.js

+4
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function $CompileProvider($provide) {
178178
* @returns {ng.$compileProvider} Self for chaining.
179179
*/
180180
this.directive = function registerDirective(name, directiveFactory) {
181+
assertNotHasOwnProperty(name, 'directive');
181182
if (isString(name)) {
182183
assertArg(directiveFactory, 'directiveFactory');
183184
if (!hasDirectives.hasOwnProperty(name)) {
@@ -1175,6 +1176,9 @@ function $CompileProvider($provide) {
11751176
dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
11761177
} else if (key == 'style') {
11771178
$element.attr('style', $element.attr('style') + ';' + value);
1179+
// `dst` will never contain hasOwnProperty as DOM parser won't let it.
1180+
// You will get an "InvalidCharacterError: DOM Exception 5" error if you
1181+
// have an attribute like "has-own-property" or "data-has-own-property", etc.
11781182
} else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
11791183
dst[key] = value;
11801184
dstAttr[key] = srcAttr[key];

src/ng/controller.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function $ControllerProvider() {
2525
* annotations in the array notation).
2626
*/
2727
this.register = function(name, constructor) {
28+
assertNotHasOwnProperty(name, 'controller');
2829
if (isObject(name)) {
2930
extend(controllers, name)
3031
} else {

src/ng/directive/form.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,12 @@ function FormController(element, attrs) {
7373
* Input elements using ngModelController do this automatically when they are linked.
7474
*/
7575
form.$addControl = function(control) {
76+
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
77+
// and not added to the scope. Now we throw an error.
78+
assertNotHasOwnProperty(control.$name, 'input');
7679
controls.push(control);
7780

78-
if (control.$name && !form.hasOwnProperty(control.$name)) {
81+
if (control.$name) {
7982
form[control.$name] = control;
8083
}
8184
};

src/ng/directive/ngRepeat.js

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
305305
key = (collection === collectionKeys) ? index : collectionKeys[index];
306306
value = collection[key];
307307
trackById = trackByIdFn(key, value, index);
308+
assertNotHasOwnProperty(trackById, '`track by` id');
308309
if(lastBlockMap.hasOwnProperty(trackById)) {
309310
block = lastBlockMap[trackById]
310311
delete lastBlockMap[trackById];
@@ -327,6 +328,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
327328

328329
// remove existing items
329330
for (key in lastBlockMap) {
331+
// lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
330332
if (lastBlockMap.hasOwnProperty(key)) {
331333
block = lastBlockMap[key];
332334
$animate.leave(block.elements);

src/ng/directive/select.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
var ngOptionsMinErr = minErr('ngOptions');
34
/**
45
* @ngdoc directive
56
* @name ng.directive:select
@@ -152,6 +153,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
152153

153154

154155
self.addOption = function(value) {
156+
assertNotHasOwnProperty(value, '"option value"');
155157
optionsMap[value] = true;
156158

157159
if (ngModelCtrl.$viewValue == value) {
@@ -300,7 +302,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
300302
var match;
301303

302304
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
303-
throw minErr('ngOptions')('iexp',
305+
throw ngOptionsMinErr('iexp',
304306
"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}",
305307
optionsExp, startingTag(selectElement));
306308
}

src/ng/parse.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ Lexer.prototype = {
290290
text: ident
291291
};
292292

293+
// OPERATORS is our own object so we don't need to use special hasOwnPropertyFn
293294
if (OPERATORS.hasOwnProperty(ident)) {
294295
token.fn = OPERATORS[ident];
295296
token.json = OPERATORS[ident];
@@ -938,6 +939,9 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
938939
}
939940

940941
function getterFn(path, csp, fullExp) {
942+
// Check whether the cache has this getter already.
943+
// We can use hasOwnProperty directly on the cache because we ensure,
944+
// see below, that the cache never stores a path called 'hasOwnProperty'
941945
if (getterFnCache.hasOwnProperty(path)) {
942946
return getterFnCache[path];
943947
}
@@ -986,7 +990,12 @@ function getterFn(path, csp, fullExp) {
986990
fn.toString = function() { return code; };
987991
}
988992

989-
return getterFnCache[path] = fn;
993+
// Only cache the value if it's not going to mess up the cache object
994+
// This is more performant that using Object.prototype.hasOwnProperty.call
995+
if (path !== 'hasOwnProperty') {
996+
getterFnCache[path] = fn;
997+
}
998+
return fn;
990999
}
9911000

9921001
///////////////////////////////////
@@ -1036,14 +1045,23 @@ function $ParseProvider() {
10361045
return function(exp) {
10371046
var lexer = new Lexer($sniffer.csp);
10381047
var parser = new Parser(lexer, $filter, $sniffer.csp);
1048+
var parsedExpression;
10391049

10401050
switch (typeof exp) {
10411051
case 'string':
10421052
if (cache.hasOwnProperty(exp)) {
10431053
return cache[exp];
10441054
}
10451055

1046-
return cache[exp] = parser.parse(exp, false);
1056+
parsedExpression = parser.parse(exp, false);
1057+
1058+
if (exp !== 'hasOwnProperty') {
1059+
// Only cache the value if it's not going to mess up the cache object
1060+
// This is more performant that using Object.prototype.hasOwnProperty.call
1061+
cache[exp] = parsedExpression;
1062+
}
1063+
1064+
return parsedExpression;
10471065

10481066
case 'function':
10491067
return exp;

src/ngMock/angular-mocks.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ angular.mock.dump = function(object) {
731731
offset = offset || ' ';
732732
var log = [offset + 'Scope(' + scope.$id + '): {'];
733733
for ( var key in scope ) {
734-
if (scope.hasOwnProperty(key) && !key.match(/^(\$|this)/)) {
734+
if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
735735
log.push(' ' + key + ': ' + angular.toJson(scope[key]));
736736
}
737737
}
@@ -1773,7 +1773,7 @@ angular.mock.clearDataCache = function() {
17731773
cache = angular.element.cache;
17741774

17751775
for(key in cache) {
1776-
if (cache.hasOwnProperty(key)) {
1776+
if (Object.prototype.hasOwnProperty.call(cache,key)) {
17771777
var handle = cache[key].handle;
17781778

17791779
handle && angular.element(handle.elem).off();

src/ngResource/resource.js

+3
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ angular.module('ngResource', ['ng']).
347347

348348
var urlParams = self.urlParams = {};
349349
forEach(url.split(/\W/), function(param){
350+
if (param === 'hasOwnProperty') {
351+
throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name.");
352+
}
350353
if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
351354
urlParams[param] = true;
352355
}

test/AngularSpec.js

+14
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,20 @@ describe('angular', function() {
417417
});
418418

419419

420+
it('should not break if obj is an array we override hasOwnProperty', function() {
421+
var obj = [];
422+
obj[0] = 1;
423+
obj[1] = 2;
424+
obj.hasOwnProperty = null;
425+
var log = [];
426+
forEach(obj, function(value, key) {
427+
log.push(key + ':' + value);
428+
});
429+
expect(log).toEqual(['0:1', '1:2']);
430+
});
431+
432+
433+
420434
it('should handle JQLite and jQuery objects like arrays', function() {
421435
var jqObject = jqLite("<p><span>s1</span><span>s2</span></p>").find("span"),
422436
log = [];

test/auto/injectorSpec.js

+18
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,24 @@ describe('injector', function() {
295295
});
296296

297297
describe('$provide', function() {
298+
299+
it('should throw an exception if we try to register a service called "hasOwnProperty"', function() {
300+
createInjector([function($provide) {
301+
expect(function() {
302+
$provide.provider('hasOwnProperty', function() { });
303+
}).toThrowMinErr('ng', 'badname');
304+
}]);
305+
});
306+
307+
it('should throw an exception if we try to register a constant called "hasOwnProperty"', function() {
308+
createInjector([function($provide) {
309+
expect(function() {
310+
$provide.constant('hasOwnProperty', {});
311+
}).toThrowMinErr('ng', 'badname');
312+
}]);
313+
});
314+
315+
298316
describe('constant', function() {
299317
it('should create configuration injectable constants', function() {
300318
var log = [];

test/loaderSpec.js

+6
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,10 @@ describe('module loader', function() {
7272
"or forgot to load it. If registering a module ensure that you specify the dependencies as the second " +
7373
"argument.");
7474
});
75+
76+
it('should complain if a module is called "hasOwnProperty', function() {
77+
expect(function() {
78+
window.angular.module('hasOwnProperty', []);
79+
}).toThrowMinErr('ng','badname', "hasOwnProperty is not a valid module name");
80+
});
7581
});

test/ng/compileSpec.js

+9
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ describe('$compile', function() {
117117
expect(log).toEqual('pre1; pre2; post2; post1');
118118
});
119119
});
120+
121+
it('should throw an exception if a directive is called "hasOwnProperty"', function() {
122+
module(function() {
123+
expect(function() {
124+
directive('hasOwnProperty', function() { });
125+
}).toThrowMinErr('ng','badname', "hasOwnProperty is not a valid directive name");
126+
});
127+
inject(function($compile) {});
128+
});
120129
});
121130

122131

test/ng/controllerSpec.js

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ describe('$controller', function() {
5757
expect(scope.foo).toBe('bar');
5858
expect(ctrl instanceof FooCtrl).toBe(true);
5959
});
60+
61+
62+
it('should throw an exception if a controller is called "hasOwnProperty"', function () {
63+
expect(function() {
64+
$controllerProvider.register('hasOwnProperty', function($scope) {});
65+
}).toThrowMinErr('ng', 'badname', "hasOwnProperty is not a valid controller name");
66+
});
6067
});
6168

6269

test/ng/directive/formSpec.js

+12
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ describe('form', function() {
137137
});
138138

139139

140+
it('should throw an exception if an input has name="hasOwnProperty"', function() {
141+
doc = jqLite(
142+
'<form name="form">'+
143+
'<input name="hasOwnProperty" ng-model="some" />'+
144+
'<input name="other" ng-model="someOther" />'+
145+
'</form>');
146+
expect(function() {
147+
$compile(doc)(scope);
148+
}).toThrowMinErr('ng', 'badname');
149+
});
150+
151+
140152
describe('preventing default submission', function() {
141153

142154
it('should prevent form submission', function() {

test/ng/directive/ngRepeatSpec.js

+8
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ describe('ngRepeat', function() {
137137
});
138138

139139

140+
it("should throw an exception if 'track by' evaluates to 'hasOwnProperty'", function() {
141+
scope.items = {age:20};
142+
$compile('<div ng-repeat="(key, value) in items track by \'hasOwnProperty\'"></div>')(scope);
143+
scope.$digest();
144+
expect($exceptionHandler.errors.shift().message).toMatch(/ng:badname/);
145+
});
146+
147+
140148
it('should track using build in $id function', function() {
141149
element = $compile(
142150
'<ul>' +

0 commit comments

Comments
 (0)