feat($compile): add support for arbitrary DOM property and event bindings

Properties:

Previously only arbitrary DOM attribute bindings were supported via interpolation such as
`my-attribute="{{expression}}"` or `ng-attr-my-attribute="{{expression}}"`, and only a set of
distinct properties could be bound. `ng-prop-*` adds support for binding expressions to any DOM
properties. For example `ng-prop-foo="x"` will assign the value of the expression `x` to the
`foo` property, and re-assign whenever the expression `x` changes.

Events:

Previously only a distinct set of DOM events could be bound using directives such as `ng-click`,
`ng-blur` etc. `ng-on-*` adds support for binding expressions to any DOM event. For example
`ng-on-bar="barOccured($event)"` will add a listener to the "bar" event and invoke the
`barOccured($event)` expression.

Since HTML attributes are case-insensitive, property and event names are specified in snake_case
for `ng-prop-*` and `ng-on-*`. For example, to bind property `fooBar` use `ng-prop-foo_bar`, to
listen to event `fooBar` use `ng-on-foo_bar`.

Fixes #16428
Fixes #16235
Closes #16614
This commit is contained in:
Jason Bedard
2018-06-25 21:23:50 -07:00
parent 88a12f8623
commit dedb10c0b8
8 changed files with 1392 additions and 76 deletions
@@ -0,0 +1,13 @@
@ngdoc error
@name $compile:ctxoverride
@fullName DOM Property Security Context Override
@description
This error occurs when the security context for a property is defined via {@link ng.$compileProvider#addPropertySecurityContext addPropertySecurityContext()} multiple times under different security contexts.
For example:
```js
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.MEDIA_URL);
$compileProvider.addPropertySecurityContext("my-element", "src", $sce.RESOURCE_URL); //throws
```
@@ -1,12 +1,12 @@
@ngdoc error
@name $compile:nodomevents
@fullName Interpolated Event Attributes
@fullName Event Attribute/Property Binding
@description
This error occurs when one tries to create a binding for event handler attributes like `onclick`, `onload`, `onsubmit`, etc.
This error occurs when one tries to create a binding for event handler attributes or properties like `onclick`, `onload`, `onsubmit`, etc.
There is no practical value in binding to these attributes and doing so only exposes your application to security vulnerabilities like XSS.
For these reasons binding to event handler attributes (all attributes that start with `on` and `formaction` attribute) is not supported.
There is no practical value in binding to these attributes/properties and doing so only exposes your application to security vulnerabilities like XSS.
For these reasons binding to event handler attributes and properties (`formaction` and all starting with `on`) is not supported.
An example code that would allow XSS vulnerability by evaluating user input in the window context could look like this:
@@ -17,4 +17,4 @@ An example code that would allow XSS vulnerability by evaluating user input in t
Since the `onclick` evaluates the value as JavaScript code in the window context, setting the `username` model to a value like `javascript:alert('PWND')` would result in script injection when the `div` is clicked.
Please use the `ng-*` or `ng-on-*` versions instead (such as `ng-click` or `ng-on-click` rather than `onclick`).
+6
View File
@@ -171,9 +171,15 @@
/* ng/q.js */
"markQExceptionHandled": false,
/* sce.js */
"SCE_CONTEXTS": false,
/* ng/directive/directives.js */
"ngDirective": false,
/* ng/directive/ngEventDirs.js */
"createEventDirective": false,
/* ng/directive/input.js */
"VALID_CLASS": false,
"INVALID_CLASS": false,
+193 -34
View File
@@ -1586,6 +1586,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return cssClassDirectivesEnabledConfig;
};
/**
* The security context of DOM Properties.
* @private
*/
var PROP_CONTEXTS = createMap();
/**
* @ngdoc method
* @name $compileProvider#addPropertySecurityContext
* @description
*
* Defines the security context for DOM properties bound by ng-prop-*.
*
* @param {string} elementName The element name or '*' to match any element.
* @param {string} propertyName The DOM property name.
* @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL`
* @returns {object} `this` for chaining
*/
this.addPropertySecurityContext = function(elementName, propertyName, ctx) {
var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase());
if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) {
throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx);
}
PROP_CONTEXTS[key] = ctx;
return this;
};
/* Default property contexts.
*
* Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58
* Changing:
* - SecurityContext.* => SCE_CONTEXTS/$sce.*
* - STYLE => CSS
* - various URL => MEDIA_URL
* - *|formAction, form|action URL => RESOURCE_URL (like the attribute)
*/
(function registerNativePropertyContexts() {
function registerContext(ctx, values) {
forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; });
}
registerContext(SCE_CONTEXTS.HTML, [
'iframe|srcdoc',
'*|innerHTML',
'*|outerHTML'
]);
registerContext(SCE_CONTEXTS.CSS, ['*|style']);
registerContext(SCE_CONTEXTS.URL, [
'area|href', 'area|ping',
'a|href', 'a|ping',
'blockquote|cite',
'body|background',
'del|cite',
'input|src',
'ins|cite',
'q|cite'
]);
registerContext(SCE_CONTEXTS.MEDIA_URL, [
'audio|src',
'img|src', 'img|srcset',
'source|src', 'source|srcset',
'track|src',
'video|src', 'video|poster'
]);
registerContext(SCE_CONTEXTS.RESOURCE_URL, [
'*|formAction',
'applet|code', 'applet|codebase',
'base|href',
'embed|src',
'frame|src',
'form|action',
'head|profile',
'html|manifest',
'iframe|src',
'link|href',
'media|src',
'object|codebase', 'object|data',
'script|src'
]);
})();
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate',
@@ -1631,12 +1716,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function sanitizeSrcset(value) {
function sanitizeSrcset(value, invokeType) {
if (!value) {
return value;
}
if (!isString(value)) {
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, value.toString());
}
// Such values are a bit too complex to handle automatically inside $sce.
@@ -1916,7 +2001,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
: function denormalizeTemplate(template) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/;
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
@@ -2252,43 +2337,66 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes,
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
var attrStartName = false;
var attrEndName = false;
var isNgAttr = false, isNgProp = false, isNgEvent = false;
var multiElementMatch;
attr = nAttrs[j];
name = attr.name;
value = attr.value;
// support ngAttr attribute binding
ngAttrName = directiveNormalize(name);
isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
if (isNgAttr) {
nName = directiveNormalize(name.toLowerCase());
// Support ng-attr-*, ng-prop-* and ng-on-*
if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) {
isNgAttr = ngPrefixMatch[1] === 'Attr';
isNgProp = ngPrefixMatch[1] === 'Prop';
isNgEvent = ngPrefixMatch[1] === 'On';
// Normalize the non-prefixed name
name = name.replace(PREFIX_REGEXP, '')
.substr(8).replace(/_(.)/g, function(match, letter) {
.toLowerCase()
.substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) {
return letter.toUpperCase();
});
}
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
// Support *-start / *-end multi element directives
} else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
if (isNgProp || isNgEvent) {
attrs[nName] = value;
attrsMap[nName] = attr.name;
if (isNgProp) {
addPropertyDirective(node, directives, nName, name);
} else {
addEventDirective(directives, nName, name);
}
} else {
// Update nName for cases where a prefix was removed
// NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-*
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
if (isNgAttr || !attrs.hasOwnProperty(nName)) {
attrs[nName] = value;
if (getBooleanAttrName(node, nName)) {
attrs[nName] = true; // presence means true
}
}
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
attrEndName);
}
addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
attrEndName);
}
if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
@@ -3332,42 +3440,95 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function getTrustedContext(node, attrNormalizedName) {
function getTrustedAttrContext(nodeName, attrNormalizedName) {
if (attrNormalizedName === 'srcdoc') {
return $sce.HTML;
}
var tag = nodeName_(node);
// All tags with src attributes require a RESOURCE_URL value, except for
// img and various html5 media tags, which require the MEDIA_URL context.
// All nodes with src attributes require a RESOURCE_URL value, except for
// img and various html5 media nodes, which require the MEDIA_URL context.
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) {
return $sce.RESOURCE_URL;
}
return $sce.MEDIA_URL;
} else if (attrNormalizedName === 'xlinkHref') {
// Some xlink:href are okay, most aren't
if (tag === 'image') return $sce.MEDIA_URL;
if (tag === 'a') return $sce.URL;
if (nodeName === 'image') return $sce.MEDIA_URL;
if (nodeName === 'a') return $sce.URL;
return $sce.RESOURCE_URL;
} else if (
// Formaction
(tag === 'form' && attrNormalizedName === 'action') ||
(nodeName === 'form' && attrNormalizedName === 'action') ||
// If relative URLs can go where they are not expected to, then
// all sorts of trust issues can arise.
(tag === 'base' && attrNormalizedName === 'href') ||
(nodeName === 'base' && attrNormalizedName === 'href') ||
// links can be stylesheets or imports, which can run script in the current origin
(tag === 'link' && attrNormalizedName === 'href')
(nodeName === 'link' && attrNormalizedName === 'href')
) {
return $sce.RESOURCE_URL;
} else if (tag === 'a' && (attrNormalizedName === 'href' ||
} else if (nodeName === 'a' && (attrNormalizedName === 'href' ||
attrNormalizedName === 'ngHref')) {
return $sce.URL;
}
}
function getTrustedPropContext(nodeName, propNormalizedName) {
var prop = propNormalizedName.toLowerCase();
return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop];
}
function sanitizeSrcsetPropertyValue(value) {
return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset');
}
function addPropertyDirective(node, directives, attrName, propName) {
if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) {
throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
}
var nodeName = nodeName_(node);
var trustedContext = getTrustedPropContext(nodeName, propName);
var sanitizer = identity;
// Sanitize img[srcset] + source[srcset] values.
if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) {
sanitizer = sanitizeSrcsetPropertyValue;
} else if (trustedContext) {
sanitizer = $sce.getTrusted.bind($sce, trustedContext);
}
directives.push({
priority: 100,
compile: function ngPropCompileFn(_, attr) {
var ngPropGetter = $parse(attr[attrName]);
var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) {
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
return $sce.valueOf(val);
});
return {
pre: function ngPropPreLinkFn(scope, $element) {
function applyPropValue() {
var propValue = ngPropGetter(scope);
$element.prop(propName, sanitizer(propValue));
}
applyPropValue();
scope.$watch(ngPropWatch, applyPropValue);
}
};
}
});
}
function addEventDirective(directives, attrName, eventName) {
directives.push(
createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false)
);
}
function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
var trustedContext = getTrustedContext(node, name);
var nodeName = nodeName_(node);
var trustedContext = getTrustedAttrContext(nodeName, name);
var mustHaveExpression = !isNgAttr;
var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
@@ -3376,16 +3537,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// no interpolation found -> ignore
if (!interpolateFn) return;
if (name === 'multiple' && nodeName_(node) === 'select') {
if (name === 'multiple' && nodeName === 'select') {
throw $compileMinErr('selmulti',
'Binding to the \'multiple\' attribute is not supported. Element: {0}',
startingTag(node));
}
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
'ng- versions (such as ng-click instead of onclick) instead.');
throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
}
directives.push({
+33 -29
View File
@@ -51,39 +51,43 @@ forEach(
function(eventName) {
var directiveName = directiveNormalize('ng-' + eventName);
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (!$rootScope.$$phase) {
scope.$apply(callback);
} else if (forceAsyncEvents[eventName]) {
scope.$evalAsync(callback);
} else {
try {
callback();
} catch (error) {
$exceptionHandler(error);
}
}
});
};
}
};
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
}];
}
);
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (!$rootScope.$$phase) {
scope.$apply(callback);
} else if (forceAsync) {
scope.$evalAsync(callback);
} else {
try {
callback();
} catch (error) {
$exceptionHandler(error);
}
}
});
};
}
};
}
/**
* @ngdoc directive
* @name ngDblclick
+148 -8
View File
@@ -11480,7 +11480,7 @@ describe('$compile', function() {
expect(element.attr('srcset')).toEqual('http://example.com');
}));
it('does not work with trusted values', inject(function($rootScope, $compile, $sce) {
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
// Use trustAsHtml and ng-bind-html to work around this.
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
@@ -11705,18 +11705,19 @@ describe('$compile', function() {
expect(function() {
$compile('<button onclick="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ONCLICK="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed. ' +
'Please use the ng- versions (such as ng-click instead of onclick) instead.');
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
}));
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
@@ -11833,7 +11834,7 @@ describe('$compile', function() {
}));
it('should pass through $sce.trustAs() values in action attribute', inject(function($compile, $rootScope, $sce) {
it('should pass through $sce.trustAsResourceUrl() values in action attribute', inject(function($compile, $rootScope, $sce) {
element = $compile('<form action="{{testUrl}}"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
@@ -12026,6 +12027,39 @@ describe('$compile', function() {
expect(element.attr('test3')).toBe('Misko');
}));
it('should use the non-prefixed name in $attr mappings', function() {
var attrs;
module(function() {
directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-attr-title="12" ng-attr-super-title="34" ng-attr-my-camel_title="56">')($rootScope);
$rootScope.$apply();
expect(attrs.title).toBe('12');
expect(attrs.$attr.title).toBe('title');
expect(attrs.ngAttrTitle).toBeUndefined();
expect(attrs.$attr.ngAttrTitle).toBeUndefined();
expect(attrs.superTitle).toBe('34');
expect(attrs.$attr.superTitle).toBe('super-title');
expect(attrs.ngAttrSuperTitle).toBeUndefined();
expect(attrs.$attr.ngAttrSuperTitle).toBeUndefined();
// Note the casing is incorrect: https://github.com/angular/angular.js/issues/16624
expect(attrs.myCameltitle).toBe('56');
expect(attrs.$attr.myCameltitle).toBe('my-camelTitle');
expect(attrs.ngAttrMyCameltitle).toBeUndefined();
expect(attrs.ngAttrMyCamelTitle).toBeUndefined();
expect(attrs.$attr.ngAttrMyCameltitle).toBeUndefined();
expect(attrs.$attr.ngAttrMyCamelTitle).toBeUndefined();
});
});
it('should work with the "href" attribute', inject(function() {
$rootScope.value = 'test';
element = $compile('<a ng-attr-href="test/{{value}}"></a>')($rootScope);
@@ -12112,6 +12146,112 @@ describe('$compile', function() {
});
describe('addPropertySecurityContext', function() {
function testProvider(provider) {
module(provider);
inject(function($compile) { /* done! */ });
}
it('should allow adding new properties', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('*', 'my-prop', 'resourceUrl');
});
});
it('should allow different sce types of a property on different element types', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('span', 'title', 'css');
$compileProvider.addPropertySecurityContext('*', 'title', 'resourceUrl');
$compileProvider.addPropertySecurityContext('article', 'title', 'html');
});
});
it('should throw \'ctxoverride\' when changing an existing context', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
expect(function() {
$compileProvider.addPropertySecurityContext('div', 'title', 'resourceUrl');
})
.toThrowMinErr('$compile', 'ctxoverride', 'Property context \'div.title\' already set to \'mediaUrl\', cannot override to \'resourceUrl\'.');
});
});
it('should allow setting the same property/element to the same value', function() {
testProvider(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
$compileProvider.addPropertySecurityContext('div', 'title', 'mediaUrl');
});
});
it('should enforce the specified sce type for properties added for specific elements', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
it('should enforce the specified sce type for properties added for all elements (*)', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('*', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
it('should enforce the specific sce type when both an element specific and generic exist', function() {
module(function($compileProvider) {
$compileProvider.addPropertySecurityContext('*', 'foo', 'css');
$compileProvider.addPropertySecurityContext('div', 'foo', 'mediaUrl');
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<div ng-prop-foo="bar"></div>')($rootScope);
$rootScope.bar = 'untrusted:test1';
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test1');
$rootScope.bar = $sce.trustAsCss('untrusted:test2');
$rootScope.$apply();
expect(element.prop('foo')).toBe('unsafe:untrusted:test2');
$rootScope.bar = $sce.trustAsMediaUrl('untrusted:test3');
$rootScope.$apply();
expect(element.prop('foo')).toBe('untrusted:test3');
});
});
});
describe('when an attribute has an underscore-separated name', function() {
it('should work with different prefixes', inject(function($compile, $rootScope) {
+158
View File
@@ -0,0 +1,158 @@
'use strict';
describe('ngOn* event binding', function() {
it('should add event listener of specified name', inject(function($compile, $rootScope) {
$rootScope.name = 'Misko';
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
element.triggerHandler('foo');
expect($rootScope.name).toBe('Misko3');
}));
it('should use angular.element(x).on() API to add listener', inject(function($compile, $rootScope) {
spyOn(angular.element.prototype, 'on');
var element = $compile('<span ng-on-foo="name = name + 3"></span>')($rootScope);
expect(angular.element.prototype.on).toHaveBeenCalledWith('foo', jasmine.any(Function));
}));
it('should allow access to the $event object', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-foo="e = $event"></span>')($rootScope);
element.triggerHandler('foo');
expect($rootScope.e.target).toBe(element[0]);
}));
it('should call the listener synchronously', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo="fooEvent()"></span>')($rootScope);
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
element.triggerHandler('foo');
expect($rootScope.fooEvent).toHaveBeenCalledOnce();
}));
it('should support multiple events on a single element', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo="fooEvent()" ng-on-bar="barEvent()"></span>')($rootScope);
$rootScope.fooEvent = jasmine.createSpy('fooEvent');
$rootScope.barEvent = jasmine.createSpy('barEvent');
element.triggerHandler('foo');
expect($rootScope.fooEvent).toHaveBeenCalled();
expect($rootScope.barEvent).not.toHaveBeenCalled();
$rootScope.fooEvent.calls.reset();
$rootScope.barEvent.calls.reset();
element.triggerHandler('bar');
expect($rootScope.fooEvent).not.toHaveBeenCalled();
expect($rootScope.barEvent).toHaveBeenCalled();
}));
it('should work with different prefixes', inject(function($rootScope, $compile) {
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
var element = $compile('<span ng:on:test="cb(1)" ng-On-test2="cb(2)" ng_On_test3="cb(3)"></span>')($rootScope);
element.triggerHandler('test');
expect(cb).toHaveBeenCalledWith(1);
element.triggerHandler('test2');
expect(cb).toHaveBeenCalledWith(2);
element.triggerHandler('test3');
expect(cb).toHaveBeenCalledWith(3);
}));
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
var element = $compile('<span data-ng-on-test2="cb(2)" x-ng-on-test3="cb(3)" data-ng:on-test4="cb(4)" ' +
'x_ng-on-test5="cb(5)" data:ng-on-test6="cb(6)"></span>')($rootScope);
element.triggerHandler('test2');
expect(cb).toHaveBeenCalledWith(2);
element.triggerHandler('test3');
expect(cb).toHaveBeenCalledWith(3);
element.triggerHandler('test4');
expect(cb).toHaveBeenCalledWith(4);
element.triggerHandler('test5');
expect(cb).toHaveBeenCalledWith(5);
element.triggerHandler('test6');
expect(cb).toHaveBeenCalledWith(6);
}));
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" asdf="foo" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" ng-attr-asdf="foo" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of properties with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-on-asdf="cb()" ng-prop-asdf="123" />')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('asdf');
expect(cb).toHaveBeenCalled();
expect(element.prop('asdf')).toBe(123);
}));
it('should use the full ng-on-* attribute name in $attr mappings', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-on-title="cb(1)" ng-on-super-title="cb(2)" ng-on-my-camel_title="cb(3)">')($rootScope);
expect(attrs.title).toBeUndefined();
expect(attrs.$attr.title).toBeUndefined();
expect(attrs.ngOnTitle).toBe('cb(1)');
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
expect(attrs.superTitle).toBeUndefined();
expect(attrs.$attr.superTitle).toBeUndefined();
expect(attrs.ngOnSuperTitle).toBe('cb(2)');
expect(attrs.$attr.ngOnSuperTitle).toBe('ng-on-super-title');
expect(attrs.myCamelTitle).toBeUndefined();
expect(attrs.$attr.myCamelTitle).toBeUndefined();
expect(attrs.ngOnMyCamelTitle).toBe('cb(3)');
expect(attrs.$attr.ngOnMyCamelTitle).toBe('ng-on-my-camel_title');
});
});
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-on-title="42" ng-attr-title="foo" title="bar">')($rootScope);
expect(attrs.title).toBe('foo');
expect(attrs.$attr.title).toBe('title');
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
});
});
});
+836
View File
@@ -0,0 +1,836 @@
'use strict';
/* eslint-disable no-script-url */
describe('ngProp*', function() {
it('should bind boolean properties (input disabled)', inject(function($rootScope, $compile) {
var element = $compile('<button ng-prop-disabled="isDisabled">Button</button>')($rootScope);
$rootScope.$digest();
expect(element.prop('disabled')).toBe(false);
$rootScope.isDisabled = true;
$rootScope.$digest();
expect(element.prop('disabled')).toBe(true);
$rootScope.isDisabled = false;
$rootScope.$digest();
expect(element.prop('disabled')).toBe(false);
}));
it('should bind boolean properties (input checked)', inject(function($rootScope, $compile) {
var element = $compile('<input type="checkbox" ng-prop-checked="isChecked" />')($rootScope);
expect(element.prop('checked')).toBe(false);
$rootScope.isChecked = true;
$rootScope.$digest();
expect(element.prop('checked')).toBe(true);
$rootScope.isChecked = false;
$rootScope.$digest();
expect(element.prop('checked')).toBe(false);
}));
it('should bind string properties (title)', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-title="title" />')($rootScope);
$rootScope.title = 123;
$rootScope.$digest();
expect(element.prop('title')).toBe('123');
$rootScope.title = 'foobar';
$rootScope.$digest();
expect(element.prop('title')).toBe('foobar');
}));
it('should bind variable type properties', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
$rootScope.asdf = 'foobar';
$rootScope.$digest();
expect(element.prop('asdf')).toBe('foobar');
$rootScope.asdf = true;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(true);
}));
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
$rootScope.value = 123;
$rootScope.$digest();
expect(element.prop('aBcdE')).toBe(123);
}));
it('should work with different prefixes', inject(function($rootScope, $compile) {
$rootScope.name = 'Misko';
var element = $compile('<span ng:prop:test="name" ng-Prop-test2="name" ng_Prop_test3="name"></span>')($rootScope);
expect(element.prop('test')).toBe('Misko');
expect(element.prop('test2')).toBe('Misko');
expect(element.prop('test3')).toBe('Misko');
}));
it('should work with the "href" property', inject(function($rootScope, $compile) {
$rootScope.value = 'test';
var element = $compile('<a ng-prop-href="\'test/\' + value"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toMatch(/\/test\/test$/);
}));
it('should work if they are prefixed with x- or data- and different prefixes', inject(function($rootScope, $compile) {
$rootScope.name = 'Misko';
var element = $compile('<span data-ng-prop-test2="name" x-ng-prop-test3="name" data-ng:prop-test4="name" ' +
'x_ng-prop-test5="name" data:ng-prop-test6="name"></span>')($rootScope);
expect(element.prop('test2')).toBe('Misko');
expect(element.prop('test3')).toBe('Misko');
expect(element.prop('test4')).toBe('Misko');
expect(element.prop('test5')).toBe('Misko');
expect(element.prop('test6')).toBe('Misko');
}));
it('should work independently of attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" asdf="foo" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
expect(element.attr('asdf')).toBe('foo');
}));
it('should work independently of (ng-)attributes with the same name', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-asdf="asdf" ng-attr-asdf="foo" />')($rootScope);
$rootScope.asdf = 123;
$rootScope.$digest();
expect(element.prop('asdf')).toBe(123);
expect(element.attr('asdf')).toBe('foo');
}));
it('should use the full ng-prop-* attribute name in $attr mappings', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-prop-title="12" ng-prop-super-title="34" ng-prop-my-camel_title="56">')($rootScope);
expect(attrs.title).toBeUndefined();
expect(attrs.$attr.title).toBeUndefined();
expect(attrs.ngPropTitle).toBe('12');
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
expect(attrs.superTitle).toBeUndefined();
expect(attrs.$attr.superTitle).toBeUndefined();
expect(attrs.ngPropSuperTitle).toBe('34');
expect(attrs.$attr.ngPropSuperTitle).toBe('ng-prop-super-title');
expect(attrs.myCamelTitle).toBeUndefined();
expect(attrs.$attr.myCamelTitle).toBeUndefined();
expect(attrs.ngPropMyCamelTitle).toBe('56');
expect(attrs.$attr.ngPropMyCamelTitle).toBe('ng-prop-my-camel_title');
});
});
it('should not conflict with (ng-attr-)attribute mappings of the same name', function() {
var attrs;
module(function($compileProvider) {
$compileProvider.directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer ng-prop-title="42" ng-attr-title="foo" title="bar">')($rootScope);
expect(attrs.title).toBe('foo');
expect(attrs.$attr.title).toBe('title');
expect(attrs.$attr.ngPropTitle).toBe('ng-prop-title');
});
});
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
// All event prop bindings are disallowed.
expect(function() {
$compile('<button ng-prop-onclick="onClickJs"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
expect(function() {
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
}));
it('should process property bindings in pre-linking phase at priority 100', function() {
module(provideLog);
module(function($compileProvider) {
$compileProvider.directive('propLog', function(log, $rootScope) {
return {
compile: function($element, $attrs) {
log('compile=' + $element.prop('myName'));
return {
pre: function($scope, $element, $attrs) {
log('preLinkP0=' + $element.prop('myName'));
$rootScope.name = 'pre0';
},
post: function($scope, $element, $attrs) {
log('postLink=' + $element.prop('myName'));
$rootScope.name = 'post0';
}
};
}
};
});
});
module(function($compileProvider) {
$compileProvider.directive('propLogHighPriority', function(log, $rootScope) {
return {
priority: 101,
compile: function() {
return {
pre: function($scope, $element, $attrs) {
log('preLinkP101=' + $element.prop('myName'));
$rootScope.name = 'pre101';
}
};
}
};
});
});
inject(function($rootScope, $compile, log) {
var element = $compile('<div prop-log-high-priority prop-log ng-prop-my_name="name"></div>')($rootScope);
$rootScope.name = 'angular';
$rootScope.$apply();
log('digest=' + element.prop('myName'));
expect(log).toEqual('compile=undefined; preLinkP101=undefined; preLinkP0=pre101; postLink=pre101; digest=angular');
});
});
['img', 'audio', 'video'].forEach(function(tag) {
// Support: IE 9 only
// IE9 rejects the `video` / `audio` tags with "Error: Not implemented"
if (msie !== 9 || tag === 'img') {
describe(tag + '[src] context requirement', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.prop('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
// As a URL
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
// As a RESOURCE URL
element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('untrusted:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = 'untrusted:foo()';
$rootScope.$digest();
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
}));
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<' + tag + ' ng-prop-src="testUrl"></' + tag + '>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
$rootScope.$digest();
expect(element.prop('src')).toEqual('unsafe:untrusted:foo()');
}));
});
}
});
// Support: IE 9 only
// IE 9 rejects the `source` / `track` tags with
// "Unable to get value of the property 'childNodes': object is null or undefined"
if (msie !== 9) {
['source', 'track'].forEach(function(tag) {
describe(tag + '[src]', function() {
it('should NOT require trusted values for whitelisted URIs', inject(function($rootScope, $compile) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('http://example.com/image.mp4');
}));
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
// As a MEDIA_URL URL
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
// As a URL
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
// As a RESOURCE URL
element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('javascript:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = 'untrusted:foo()';
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
}));
it('should sanitize wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<video><' + tag + ' ng-prop-src="testUrl"></' + tag + '></video>')($rootScope);
$rootScope.testUrl = $sce.trustAsCss('untrusted:foo()');
$rootScope.$digest();
expect(element.find(tag).prop('src')).toEqual('unsafe:untrusted:foo()');
}));
});
});
}
describe('img[src] sanitization', function() {
it('should accept trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
// Some browsers complain if you try to write `javascript:` into an `img[src]`
// So for the test use something different
$rootScope.testUrl = $sce.trustAsMediaUrl('someuntrustedthing:foo();');
$rootScope.$digest();
expect(element.prop('src')).toEqual('someuntrustedthing:foo();');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('src')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
});
});
it('should not use $$sanitizeUri with trusted values', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.throwError('Should not have been called');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope, $sce) {
var element = $compile('<img ng-prop-src="testUrl"></img>')($rootScope);
// Assigning javascript:foo to src makes at least IE9-11 complain, so use another
// protocol name.
$rootScope.testUrl = $sce.trustAsMediaUrl('untrusted:foo();');
$rootScope.$apply();
expect(element.prop('src')).toBe('untrusted:foo();');
});
});
});
['img', 'source'].forEach(function(srcsetElement) {
// Support: IE 9 only
// IE9 ignores source[srcset] property assignments
if (msie !== 9 || srcsetElement === 'img') {
describe(srcsetElement + '[srcset] sanitization', function() {
it('should not error if srcset is blank', inject(function($compile, $rootScope) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
// Set srcset to a value
$rootScope.testUrl = 'http://example.com/';
$rootScope.$digest();
expect(element.prop('srcset')).toBe('http://example.com/');
// Now set it to blank
$rootScope.testUrl = '';
$rootScope.$digest();
expect(element.prop('srcset')).toBe('');
}));
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('http://example.com');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('http://example.com');
}));
it('should NOT work with trusted values', inject(function($rootScope, $compile, $sce) {
// A limitation of the approach used for srcset is that you cannot use `trustAsUrl`.
// Use trustAsHtml and ng-bind-html to work around this.
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual('unsafe:javascript:something');
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:something');
$rootScope.$digest();
expect(element.prop('srcset')).toEqual(
'unsafe:javascript:something ,unsafe:javascript:something');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('srcset')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true);
element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl + \',\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'javascript:yay';
$rootScope.$apply();
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
element = $compile('<' + srcsetElement + ' ng-prop-srcset="\'java\' + testUrl"></' + srcsetElement + '>')($rootScope);
$rootScope.testUrl = 'script:yay, javascript:nay';
$rootScope.$apply();
expect(element.prop('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl');
});
});
it('should sanitize all uris in srcset', inject(function($rootScope, $compile) {
var element = $compile('<' + srcsetElement + ' ng-prop-srcset="testUrl"></' + srcsetElement + '>')($rootScope);
var testSet = {
'http://example.com/image.png':'http://example.com/image.png',
' http://example.com/image.png':'http://example.com/image.png',
'http://example.com/image.png ':'http://example.com/image.png',
'http://example.com/image.png 128w':'http://example.com/image.png 128w',
'http://example.com/image.png 2x':'http://example.com/image.png 2x',
'http://example.com/image.png 1.5x':'http://example.com/image.png 1.5x',
'http://example.com/image1.png 1x,http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x ,http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x':'http://example.com/image1.png 1x,http://example.com/image2.png 2x',
'http://example.com/image1.png 1x , http://example.com/image2.png 2x':'http://example.com/image1.png 1x ,http://example.com/image2.png 2x',
'http://example.com/image1.png 48w,http://example.com/image2.png 64w':'http://example.com/image1.png 48w,http://example.com/image2.png 64w',
//Test regex to make sure doesn't mistake parts of url for width descriptors
'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w':'http://example.com/image1.png?w=48w,http://example.com/image2.png 64w',
'http://example.com/image1.png 1x,http://example.com/image2.png 64w':'http://example.com/image1.png 1x,http://example.com/image2.png 64w',
'http://example.com/image1.png,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png ,http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png, http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png , http://example.com/image2.png':'http://example.com/image1.png ,http://example.com/image2.png',
'http://example.com/image1.png 1x, http://example.com/image2.png 2x, http://example.com/image3.png 3x':
'http://example.com/image1.png 1x,http://example.com/image2.png 2x,http://example.com/image3.png 3x',
'javascript:doEvilStuff() 2x': 'unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.png 1x,javascript:doEvilStuff() 2x':'http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x',
'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a,b 1x,http://example.com/ima,ge2.jpg 2x',
//Test regex to make sure doesn't mistake parts of url for pixel density descriptors
'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x':'http://example.com/image1.jpg?x=a2x,b 1x,http://example.com/ima,ge2.jpg 2x'
};
forEach(testSet, function(ref, url) {
$rootScope.testUrl = url;
$rootScope.$digest();
expect(element.prop('srcset')).toEqual(ref);
});
}));
});
}
});
describe('a[href] sanitization', function() {
it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('http://example.com/image.png');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('http://example.com/image.png');
}));
it('should accept trusted values for non-whitelisted values', inject(function($rootScope, $compile, $sce) {
$rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); // `javascript` is not whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('javascript:foo()');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('javascript:foo()');
}));
it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) {
$rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$digest();
expect(element.prop('href')).toEqual('unsafe:javascript:foo()');
}));
it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) {
var element = $compile('<div ng-prop-href="testUrl"></div>')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.prop('href')).toBe('javascript:doEvilStuff()');
}));
it('should not sanitize properties other then those configured', inject(function($compile, $rootScope) {
var element = $compile('<a ng-prop-title="testUrl"></a>')($rootScope);
$rootScope.testUrl = 'javascript:doEvilStuff()';
$rootScope.$apply();
expect(element.prop('title')).toBe('javascript:doEvilStuff()');
}));
it('should use $$sanitizeUri', function() {
var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl');
module(function($provide) {
$provide.value('$$sanitizeUri', $$sanitizeUri);
});
inject(function($compile, $rootScope) {
var element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.testUrl = 'someUrl';
$rootScope.$apply();
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
$$sanitizeUri.calls.reset();
element = $compile('<a ng-prop-href="testUrl"></a>')($rootScope);
$rootScope.$apply();
expect(element.prop('href')).toMatch(/^http:\/\/.*\/someSanitizedUrl$/);
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) {
var element = $compile('<foo ng-prop-href="testUrl"></foo><foo ng-prop-href="::testUrl"></foo>' +
'<foo ng-prop-href="\'http://example.com/\' + testUrl"></foo><foo ng-prop-href="::\'http://example.com/\' + testUrl"></foo>')($rootScope);
$rootScope.testUrl = [1];
$rootScope.$digest();
$rootScope.testUrl = [];
$rootScope.$digest();
$rootScope.testUrl = {a:'b'};
$rootScope.$digest();
$rootScope.testUrl = {};
$rootScope.$digest();
}));
});
describe('iframe[src]', function() {
it('should pass through src properties for the same domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.prop('src')).toMatch(/\/different_page$/);
}));
it('should clear out src properties for a different domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: http://a.different.domain.example.com');
}));
it('should clear out JS src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:alert(1);');
}));
it('should clear out non-resource_url src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAs() values in src properties', inject(function($compile, $rootScope, $sce) {
var element = $compile('<iframe ng-prop-src="testUrl"></iframe>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.prop('src')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('base[href]', function() {
it('should be a RESOURCE_URL context', inject(function($compile, $rootScope, $sce) {
var element = $compile('<base ng-prop-href="testUrl"/>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('https://example.com/');
$rootScope.$apply();
expect(element.prop('href')).toContain('https://example.com/');
$rootScope.testUrl = 'https://not.example.com/';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: https://not.example.com/');
}));
});
describe('form[action]', function() {
it('should pass through action property for the same domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'different_page';
$rootScope.$apply();
expect(element.prop('action')).toMatch(/\/different_page$/);
}));
it('should clear out action property for a different domain', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'http://a.different.domain.example.com';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: http://a.different.domain.example.com');
}));
it('should clear out JS action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = 'javascript:alert(1);';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:alert(1);');
}));
it('should clear out non-resource_url action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsUrl('javascript:doTrustedStuff()');
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: javascript:doTrustedStuff()');
}));
it('should pass through $sce.trustAsResourceUrl() values in action property', inject(function($compile, $rootScope, $sce) {
var element = $compile('<form ng-prop-action="testUrl"></form>')($rootScope);
$rootScope.testUrl = $sce.trustAsResourceUrl('javascript:doTrustedStuff()');
$rootScope.$apply();
expect(element.prop('action')).toEqual('javascript:doTrustedStuff()');
}));
});
describe('link[href]', function() {
it('should reject invalid RESOURCE_URLs', inject(function($compile, $rootScope) {
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
$rootScope.testUrl = 'https://evil.example.org/css.css';
expect(function() { $rootScope.$apply(); }).toThrowMinErr(
'$sce', 'insecurl', 'Blocked loading resource from url not allowed by $sceDelegate policy.' +
' URL: https://evil.example.org/css.css');
}));
it('should accept valid RESOURCE_URLs', inject(function($compile, $rootScope, $sce) {
var element = $compile('<link ng-prop-href="testUrl" rel="stylesheet" />')($rootScope);
$rootScope.testUrl = './css1.css';
$rootScope.$apply();
expect(element.prop('href')).toContain('css1.css');
$rootScope.testUrl = $sce.trustAsResourceUrl('https://elsewhere.example.org/css2.css');
$rootScope.$apply();
expect(element.prop('href')).toContain('https://elsewhere.example.org/css2.css');
}));
});
describe('*[innerHTML]', function() {
describe('SCE disabled', function() {
beforeEach(function() {
module(function($sceProvider) { $sceProvider.enabled(false); });
});
it('should set html', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should update html', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = 'hello';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
$rootScope.html = 'goodbye';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
}));
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="::html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect(element.text()).toEqual('hello');
expect($rootScope.$$watchers.length).toEqual(0);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(element.text()).toEqual('hello');
}));
});
describe('SCE enabled', function() {
it('should NOT set html for untrusted values', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should NOT set html for wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsCss('<div onclick="">hello</div>');
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should set html for trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsHtml('<div onclick="">hello</div>');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should update html', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = $sce.trustAsHtml('hello');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
$rootScope.html = $sce.trustAsHtml('goodbye');
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
}));
it('should not cause infinite recursion for trustAsHtml object watches',
inject(function($rootScope, $compile, $sce) {
// Ref: https://github.com/angular/angular.js/issues/3932
// If the binding is a function that creates a new value on every call via trustAs, we'll
// trigger an infinite digest if we don't take care of it.
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
$rootScope.getHtml = function() {
return $sce.trustAsHtml('<div onclick="">hello</div>');
};
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should handle custom $sce objects', function() {
function MySafeHtml(val) { this.val = val; }
module(function($provide) {
$provide.decorator('$sce', function($delegate) {
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
$delegate.getTrusted = function(type, mySafeHtml) { return mySafeHtml && mySafeHtml.val; };
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
return $delegate;
});
});
inject(function($rootScope, $compile, $sce) {
// Ref: https://github.com/angular/angular.js/issues/14526
// Previous code used toString for change detection, which fails for custom objects
// that don't override toString.
var element = $compile('<div ng-prop-inner_h_t_m_l="getHtml()"></div>')($rootScope);
var html = 'hello';
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('hello');
html = 'goodbye';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('goodbye');
});
});
describe('when $sanitize is available', function() {
beforeEach(function() { module('ngSanitize'); });
it('should sanitize untrusted html', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-inner_h_t_m_l="html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('<div>hello</div>');
}));
});
});
});
describe('*[style]', function() {
// Support: IE9
// Some browsers throw when assignging to HTMLElement.style
function canAssignStyleProp() {
try {
window.document.createElement('div').style = 'margin-left: 10px';
return true;
} catch (e) {
return false;
}
}
it('should NOT set style for untrusted values', inject(function($rootScope, $compile) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = 'margin-left: 10px';
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
it('should NOT set style for wrongly typed values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = $sce.trustAsHtml('margin-left: 10px');
expect(function() { $rootScope.$digest(); }).toThrowMinErr('$sce', 'unsafe', 'Attempting to use an unsafe value in a safe context.');
}));
if (canAssignStyleProp()) {
it('should set style for trusted values', inject(function($rootScope, $compile, $sce) {
var element = $compile('<div ng-prop-style="style"></div>')($rootScope);
$rootScope.style = $sce.trustAsCss('margin-left: 10px');
$rootScope.$digest();
// Support: IE
// IE allows assignments but does not register the styles
// Sometimes the value is '0px', sometimes ''
if (msie) {
expect(parseInt(element.css('margin-left'), 10) || 0).toBe(0);
} else {
expect(element.css('margin-left')).toEqual('10px');
}
}));
}
});
});