diff --git a/docs/content/error/$compile/ctxoverride.ngdoc b/docs/content/error/$compile/ctxoverride.ngdoc
new file mode 100644
index 000000000000..839b304d47a3
--- /dev/null
+++ b/docs/content/error/$compile/ctxoverride.ngdoc
@@ -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
+```
diff --git a/docs/content/error/$compile/nodomevents.ngdoc b/docs/content/error/$compile/nodomevents.ngdoc
index ed1888c73956..283bd76fa669 100644
--- a/docs/content/error/$compile/nodomevents.ngdoc
+++ b/docs/content/error/$compile/nodomevents.ngdoc
@@ -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`).
diff --git a/src/.eslintrc.json b/src/.eslintrc.json
index cf2331ef6f98..dca5e4b006ef 100644
--- a/src/.eslintrc.json
+++ b/src/.eslintrc.json
@@ -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,
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 7a6366f9f29b..e259cb3efcbe 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -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,6 +1716,57 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
+ function sanitizeSrcset(value, invokeType) {
+ if (!value) {
+ return value;
+ }
+ if (!isString(value)) {
+ 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.
+ // Instead, we sanitize each of the URIs individually, which works, even dynamically.
+
+ // It's not possible to work around this using `$sce.trustAsMediaUrl`.
+ // If you want to programmatically set explicitly trusted unsafe URLs, you should use
+ // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
+ // `ng-bind-html` directive.
+
+ var result = '';
+
+ // first check if there are spaces because it's not the same pattern
+ var trimmedSrcset = trim(value);
+ // ( 999x ,| 999w ,| ,|, )
+ var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
+ var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
+
+ // split srcset into tuple of uri and descriptor except for the last item
+ var rawUris = trimmedSrcset.split(pattern);
+
+ // for each tuples
+ var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
+ for (var i = 0; i < nbrUrisWith2parts; i++) {
+ var innerIdx = i * 2;
+ // sanitize the uri
+ result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
+ // add the descriptor
+ result += ' ' + trim(rawUris[innerIdx + 1]);
+ }
+
+ // split the last item into uri and descriptor
+ var lastTuple = trim(rawUris[i * 2]).split(/\s/);
+
+ // sanitize the last uri
+ result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
+
+ // and add the last descriptor if any
+ if (lastTuple.length === 2) {
+ result += (' ' + trim(lastTuple[1]));
+ }
+ return result;
+ }
+
+
function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
@@ -1767,51 +1903,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
// Sanitize img[srcset] values.
- if (nodeName === 'img' && key === 'srcset' && value) {
- if (!isString(value)) {
- throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
- }
-
- // Such values are a bit too complex to handle automatically inside $sce.
- // Instead, we sanitize each of the URIs individually, which works, even dynamically.
-
- // It's not possible to work around this using `$sce.trustAsMediaUrl`.
- // If you want to programmatically set explicitly trusted unsafe URLs, you should use
- // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
- // `ng-bind-html` directive.
-
- var result = '';
-
- // first check if there are spaces because it's not the same pattern
- var trimmedSrcset = trim(value);
- // ( 999x ,| 999w ,| ,|, )
- var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
- var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
-
- // split srcset into tuple of uri and descriptor except for the last item
- var rawUris = trimmedSrcset.split(pattern);
-
- // for each tuples
- var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
- for (var i = 0; i < nbrUrisWith2parts; i++) {
- var innerIdx = i * 2;
- // sanitize the uri
- result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
- // add the descriptor
- result += ' ' + trim(rawUris[innerIdx + 1]);
- }
-
- // split the last item into uri and descriptor
- var lastTuple = trim(rawUris[i * 2]).split(/\s/);
-
- // sanitize the last uri
- result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
-
- // and add the last descriptor if any
- if (lastTuple.length === 2) {
- result += (' ' + trim(lastTuple[1]));
- }
- this[key] = value = result;
+ if (nodeName === 'img' && key === 'srcset') {
+ this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)');
}
if (writeAttr !== false) {
@@ -1908,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) {
@@ -2244,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') {
@@ -3324,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;
@@ -3368,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({
diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js
index faee43da465a..dbb7c4b1bda1 100644
--- a/src/ng/directive/ngEventDirs.js
+++ b/src/ng/directive/ngEventDirs.js
@@ -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
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index a0b8b7204439..48b5e565e088 100644
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -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('')($rootScope);
@@ -11705,18 +11705,19 @@ describe('$compile', function() {
expect(function() {
$compile('