diff --git a/docs/content/error/$compile/srcset.ngdoc b/docs/content/error/$compile/srcset.ngdoc
new file mode 100644
index 000000000000..cab3de5f4d79
--- /dev/null
+++ b/docs/content/error/$compile/srcset.ngdoc
@@ -0,0 +1,12 @@
+@ngdoc error
+@name $compile:srcset
+@fullName Invalid value passed to `attr.$set('srcset', value)`
+@description
+
+This error occurs if you try to programmatically set the `srcset` attribute with a non-string value.
+
+This can be the case if you tried to avoid the automatic sanitization of the `srcset` value by
+passing a "trusted" value provided by calls to `$sce.trustAsMediaUrl(value)`.
+
+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.
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 4ec3ea5d6d94..6ae2722a6fde 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -1528,9 +1528,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
+ '$controller', '$rootScope', '$sce', '$animate',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
+ $controller, $rootScope, $sce, $animate) {
var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = window.document.createElement('div');
@@ -1679,8 +1679,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
$set: function(key, value, writeAttr, attrName) {
// TODO: decide whether or not to throw an error if "class"
- //is set through this function since it may cause $updateClass to
- //become unstable.
+ // is set through this function since it may cause $updateClass to
+ // become unstable.
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
@@ -1710,13 +1710,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
- if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
- (nodeName === 'img' && key === 'src') ||
- (nodeName === 'image' && key === 'xlinkHref')) {
- // sanitize a[href] and img[src] values
- this[key] = value = $$sanitizeUri(value, nodeName === 'img' || nodeName === 'image');
- } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
- // sanitize img[srcset] values
+ // 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
@@ -1733,16 +1740,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
for (var i = 0; i < nbrUrisWith2parts; i++) {
var innerIdx = i * 2;
// sanitize the uri
- result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
+ result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
// add the descriptor
- result += (' ' + trim(rawUris[innerIdx + 1]));
+ 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 += $$sanitizeUri(trim(lastTuple[0]), true);
+ result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
// and add the last descriptor if any
if (lastTuple.length === 2) {
@@ -3268,14 +3275,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
var tag = nodeName_(node);
// All tags with src attributes require a RESOURCE_URL value, except for
- // img and various html5 media tags.
+ // img and various html5 media tags, which require the MEDIA_URL context.
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -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;
+ return $sce.RESOURCE_URL;
} else if (
- // Some xlink:href are okay, most aren't
- (attrNormalizedName === 'xlinkHref' && (tag !== 'image' && tag !== 'a')) ||
// Formaction
(tag === 'form' && attrNormalizedName === 'action') ||
// If relative URLs can go where they are not expected to, then
@@ -3285,6 +3296,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
(tag === 'link' && attrNormalizedName === 'href')
) {
return $sce.RESOURCE_URL;
+ } else if (tag === 'a' && (attrNormalizedName === 'href' ||
+ attrNormalizedName === 'ngHref')) {
+ return $sce.URL;
}
}
diff --git a/src/ng/directive/attrs.js b/src/ng/directive/attrs.js
index af0bf14efd1f..1b646ff5d4c3 100644
--- a/src/ng/directive/attrs.js
+++ b/src/ng/directive/attrs.js
@@ -436,7 +436,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
// On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
- // We use attr[attrName] value since $set can sanitize the url.
+ // We use attr[attrName] value since $set might have sanitized the url.
if (msie && propName) element.prop(propName, attr[name]);
});
}
diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js
index 30ad9e3a9ad8..d1fe431bfa18 100644
--- a/src/ng/interpolate.js
+++ b/src/ng/interpolate.js
@@ -256,11 +256,13 @@ function $InterpolateProvider() {
endIndex,
index = 0,
expressions = [],
- parseFns = [],
+ parseFns,
textLength = text.length,
exp,
concat = [],
- expressionPositions = [];
+ expressionPositions = [],
+ singleExpression,
+ contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
while (index < textLength) {
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
@@ -270,10 +272,9 @@ function $InterpolateProvider() {
}
exp = text.substring(startIndex + startSymbolLength, endIndex);
expressions.push(exp);
- parseFns.push($parse(exp, parseStringifyInterceptor));
index = endIndex + endSymbolLength;
expressionPositions.push(concat.length);
- concat.push('');
+ concat.push(''); // Placeholder that will get replaced with the evaluated expression.
} else {
// we did not find an interpolation, so we have to add the remainder to the separators array
if (index !== textLength) {
@@ -283,15 +284,25 @@ function $InterpolateProvider() {
}
}
+ singleExpression = concat.length === 1 && expressionPositions.length === 1;
+ // Intercept expression if we need to stringify concatenated inputs, which may be SCE trusted
+ // objects rather than simple strings
+ // (we don't modify the expression if the input consists of only a single trusted input)
+ var interceptor = contextAllowsConcatenation && singleExpression ? undefined : parseStringifyInterceptor;
+ parseFns = expressions.map(function(exp) { return $parse(exp, interceptor); });
+
// Concatenating expressions makes it hard to reason about whether some combination of
// concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
- // single expression be used for iframe[src], object[src], etc., we ensure that the value
- // that's used is assigned or constructed by some JS code somewhere that is more testable or
- // make it obvious that you bound the value to some user controlled value. This helps reduce
- // the load when auditing for XSS issues.
- if (trustedContext && concat.length > 1) {
- $interpolateMinErr.throwNoconcat(text);
- }
+ // single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
+ // we ensure that the value that's used is assigned or constructed by some JS code somewhere
+ // that is more testable or make it obvious that you bound the value to some user controlled
+ // value. This helps reduce the load when auditing for XSS issues.
+
+ // Note that URL and MEDIA_URL $sce contexts do not need this, since `$sce` can sanitize the values
+ // passed to it. In that case, `$sce.getTrusted` will be called on either the single expression
+ // or on the overall concatenated string (losing trusted types used in the mix, by design).
+ // Both these methods will sanitize plain strings. Also, HTML could be included, but since it's
+ // only used in srcdoc attributes, this would not be very useful.
if (!mustHaveExpression || expressions.length) {
var compute = function(values) {
@@ -299,13 +310,16 @@ function $InterpolateProvider() {
if (allOrNothing && isUndefined(values[i])) return;
concat[expressionPositions[i]] = values[i];
}
- return concat.join('');
- };
- var getValue = function(value) {
- return trustedContext ?
- $sce.getTrusted(trustedContext, value) :
- $sce.valueOf(value);
+ if (contextAllowsConcatenation) {
+ // If `singleExpression` then `concat[0]` might be a "trusted" value or `null`, rather than a string
+ return $sce.getTrusted(trustedContext, singleExpression ? concat[0] : concat.join(''));
+ } else if (trustedContext && concat.length > 1) {
+ // This context does not allow more than one part, e.g. expr + string or exp + exp.
+ $interpolateMinErr.throwNoconcat(text);
+ }
+ // In an unprivileged context or only one part: just concatenate and return.
+ return concat.join('');
};
return extend(function interpolationFn(context) {
@@ -340,7 +354,13 @@ function $InterpolateProvider() {
function parseStringifyInterceptor(value) {
try {
- value = getValue(value);
+ // In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
+ // parts of a full URL. We don't care about losing the trustedness here.
+ // In non-concatenable contexts, where there is only one expression, this interceptor is
+ // not applied to the expression.
+ value = (trustedContext && !contextAllowsConcatenation) ?
+ $sce.getTrusted(trustedContext, value) :
+ $sce.valueOf(value);
return allOrNothing && !isDefined(value) ? value : stringify(value);
} catch (err) {
$exceptionHandler($interpolateMinErr.interr(text, err));
diff --git a/src/ng/sanitizeUri.js b/src/ng/sanitizeUri.js
index f7dc60bf3c41..b8b2d8bcdbd1 100644
--- a/src/ng/sanitizeUri.js
+++ b/src/ng/sanitizeUri.js
@@ -6,6 +6,7 @@
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {
+
var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/,
imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
@@ -14,12 +15,16 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during a[href] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
+ *
+ * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
+ * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
+ * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
+ *
+ * If the URL to matches the `aHrefSanitizationWhitelist` regular expression, it is returned unchanged.
*
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
@@ -39,12 +44,16 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during img[src] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
+ *
+ * Any url due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
+ * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
+ * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
+ *
+ * If the URL to matches the `aHrefSanitizationWhitelist` regular expression, it is returned unchanged.
*
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
@@ -59,8 +68,8 @@ function $$SanitizeUriProvider() {
};
this.$get = function() {
- return function sanitizeUri(uri, isImage) {
- var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
+ return function sanitizeUri(uri, isMediaUrl) {
+ var regex = isMediaUrl ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
var normalizedVal;
normalizedVal = urlResolve(uri && uri.trim()).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
diff --git a/src/ng/sce.js b/src/ng/sce.js
index 4dc0279fb61e..474f82a3092f 100644
--- a/src/ng/sce.js
+++ b/src/ng/sce.js
@@ -22,12 +22,17 @@ var SCE_CONTEXTS = {
// Style statements or stylesheets. Currently unused in AngularJS.
CSS: 'css',
- // An URL used in a context where it does not refer to a resource that loads code. Currently
- // unused in AngularJS.
+ // An URL used in a context where it refers to the source of media, which are not expected to be run
+ // as scripts, such as an image, audio, video, etc.
+ MEDIA_URL: 'mediaUrl',
+
+ // An URL used in a context where it does not refer to a resource that loads code.
+ // A value that is trusted as a URL is also trusted as a MEDIA_URL.
URL: 'url',
// RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
// code. (e.g. ng-include, script src binding, templateUrl)
+ // A value that is trusted as a RESOURCE_URL, is also trusted as a URL and a MEDIA_URL.
RESOURCE_URL: 'resourceUrl',
// Script. Currently unused in AngularJS.
@@ -242,7 +247,7 @@ function $SceDelegateProvider() {
return resourceUrlBlacklist;
};
- this.$get = ['$injector', function($injector) {
+ this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) {
var htmlSanitizer = function htmlSanitizer(html) {
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
@@ -307,7 +312,8 @@ function $SceDelegateProvider() {
byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
- byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]);
byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
@@ -386,15 +392,27 @@ function $SceDelegateProvider() {
* @name $sceDelegate#getTrusted
*
* @description
- * Takes any input, and either returns a value that's safe to use in the specified context, or
- * throws an exception.
+ * Given an object and a security context in which to assign it, returns a value that's safe to
+ * use in this context, which was represented by the parameter. To do so, this function either
+ * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on
+ * the context and sanitizer availablility.
+ *
+ * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available
+ * by default, and the third one relies on the `$sanitize` service (which may be loaded through
+ * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be
+ * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#resourceUrlWhitelist
+ * `$sceDelegateProvider.resourceUrlWhitelist`} and {@link ng.$sceDelegateProvider#resourceUrlBlacklist
+ * `$sceDelegateProvider.resourceUrlBlacklist`} accepts that resource.
+ *
+ * This function will throw if the safe type isn't appropriate for this context, or if the
+ * value given cannot be accepted in the context (which might be caused by sanitization not
+ * being available, or the value not being recognized as safe).
*
- * In practice, there are several cases. When given a string, this function runs checks
- * and sanitization to make it safe without prior assumptions. When given the result of a {@link
- * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied
- * value if that value's context is valid for this call's context. Finally, this function can
- * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization
- * is available or possible.)
+ *
+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ *