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. + *
* * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs @@ -412,12 +430,18 @@ function $SceDelegateProvider() { if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // Otherwise, if we get here, then we may either make it safe, or throw an exception. This - // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL), - // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS - // has no corresponding sinks. - if (type === SCE_CONTEXTS.RESOURCE_URL) { - // RESOURCE_URL uses a whitelist. + + // If maybeTrusted is a trusted class instance but not of the correct trusted type + // then unwrap it and allow it to pass through to the rest of the checks + if (isFunction(maybeTrusted.$$unwrapTrustedValue)) { + maybeTrusted = maybeTrusted.$$unwrapTrustedValue(); + } + + // If we get here, then we will either sanitize the value or throw an exception. + if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) { + // we attempt to sanitize non-resource URLs + return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL); + } else if (type === SCE_CONTEXTS.RESOURCE_URL) { if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { @@ -572,9 +596,10 @@ function $SceDelegateProvider() { * * If your expressions are constant literals, they're automatically trusted and you don't need to * call `$sce.trustAs` on them (e.g. - * `
`) just works. The `$sceDelegate` will - * also use the `$sanitize` service if it is available when binding untrusted values to - * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you + * `
`) just works (remember to include the + * `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available + * when binding untrusted values to `$sce.HTML` context. + * AngularJS provides an implementation in `angular-sanitize.js`, and if you * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in * your application. * @@ -594,17 +619,27 @@ function $SceDelegateProvider() { * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. | - * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. | - * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | - * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | + * | `$sce.MEDIA_URL` | For URLs that are safe to display as media. Is automatically converted from string by sanitizing when needed. | + * | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. | + * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.)

Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | + * + * + *
+ * Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their + * interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}. * + * **As of 1.7.0, this is no longer the case.** + * + * Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL` + * (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate` + * service evaluates the expressions. + *
* - * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings - * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This - * might evolve. + * There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs` + * functions aren't useful yet. This might evolve. * * ### Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
* @@ -778,7 +813,7 @@ function $SceProvider() { * such a value. * * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by + * This function should return the value that is safe to use in the context specified by * contextEnum or throw and exception otherwise. * * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index 1b848279d318..655333a40c6e 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -41,12 +41,11 @@ var htmlSanitizeWriter; * Sanitizes an html string by stripping all potentially dangerous tokens. * * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make + * then serialized back to a properly escaped HTML string. This means that no unsafe input can make * it into the returned string. * * The whitelist for URL sanitization of attribute values is configured using the functions - * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider - * `$compileProvider`}. + * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link $compileProvider}. * * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. * diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index ca2d43380e8f..73446d8ca6c8 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -151,13 +151,34 @@ describe('$compile', function() { describe('configuration', function() { - it('should allow aHrefSanitizationWhitelist to be configured', function() { - module(function($compileProvider) { - expect($compileProvider.aHrefSanitizationWhitelist()).toEqual(/^\s*(https?|s?ftp|mailto|tel|file):/); // the default - $compileProvider.aHrefSanitizationWhitelist(/other/); - expect($compileProvider.aHrefSanitizationWhitelist()).toEqual(/other/); + it('should use $$sanitizeUriProvider for reconfiguration of the `aHrefSanitizationWhitelist`', function() { + module(function($compileProvider, $$sanitizeUriProvider) { + var newRe = /safe:/, returnVal; + + expect($compileProvider.aHrefSanitizationWhitelist()).toBe($$sanitizeUriProvider.aHrefSanitizationWhitelist()); + returnVal = $compileProvider.aHrefSanitizationWhitelist(newRe); + expect(returnVal).toBe($compileProvider); + expect($$sanitizeUriProvider.aHrefSanitizationWhitelist()).toBe(newRe); + expect($compileProvider.aHrefSanitizationWhitelist()).toBe(newRe); + }); + inject(function() { + // needed to the module definition above is run... + }); + }); + + it('should use $$sanitizeUriProvider for reconfiguration of the `imgSrcSanitizationWhitelist`', function() { + module(function($compileProvider, $$sanitizeUriProvider) { + var newRe = /safe:/, returnVal; + + expect($compileProvider.imgSrcSanitizationWhitelist()).toBe($$sanitizeUriProvider.imgSrcSanitizationWhitelist()); + returnVal = $compileProvider.imgSrcSanitizationWhitelist(newRe); + expect(returnVal).toBe($compileProvider); + expect($$sanitizeUriProvider.imgSrcSanitizationWhitelist()).toBe(newRe); + expect($compileProvider.imgSrcSanitizationWhitelist()).toBe(newRe); + }); + inject(function() { + // needed to the module definition above is run... }); - inject(); }); it('should allow debugInfoEnabled to be configured', function() { @@ -4135,12 +4156,15 @@ describe('$compile', function() { var attr; beforeEach(function() { module(function() { - directive('input', valueFn({ - restrict: 'ECA', - link: function(scope, element, attr) { - scope.attr = attr; - } - })); + // Create directives that capture the `attr` object + ['input', 'a', 'img'].forEach(function(tag) { + directive(tag, valueFn({ + restrict: 'ECA', + link: function(scope, element, attr) { + scope.attr = attr; + } + })); + }); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); @@ -4187,6 +4211,37 @@ describe('$compile', function() { expect(element.attr('test')).toBeUndefined(); expect(attr.test).toBe('value'); }); + + it('should not automatically sanitize a[href]', inject(function($compile, $rootScope) { + // Breaking change in https://github.com/angular/angular.js/pull/16378 + element = $compile('')($rootScope); + $rootScope.attr.$set('href', 'evil:foo()'); + expect(element.attr('href')).toEqual('evil:foo()'); + expect($rootScope.attr.href).toEqual('evil:foo()'); + })); + + it('should not automatically sanitize img[src]', inject(function($compile, $rootScope) { + // Breaking change in https://github.com/angular/angular.js/pull/16378 + element = $compile('')($rootScope); + $rootScope.attr.$set('img', 'evil:foo()'); + expect(element.attr('img')).toEqual('evil:foo()'); + expect($rootScope.attr.img).toEqual('evil:foo()'); + })); + + it('should automatically sanitize img[srcset]', inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + $rootScope.attr.$set('srcset', 'evil:foo()'); + expect(element.attr('srcset')).toEqual('unsafe:evil:foo()'); + expect($rootScope.attr.srcset).toEqual('unsafe:evil:foo()'); + })); + + it('should not accept trusted values for img[srcset]', inject(function($compile, $rootScope, $sce) { + var trusted = $sce.trustAsMediaUrl('trustme:foo()'); + element = $compile('')($rootScope); + expect(function() { + $rootScope.attr.$set('srcset', trusted); + }).toThrowMinErr('$compile', 'srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "trustme:foo()"'); + })); }); }); @@ -11028,91 +11083,114 @@ describe('$compile', function() { ); }); - describe('*[src] context requirement', function() { - - it('should NOT require trusted values for img src', inject(function($rootScope, $compile, $sce) { - element = $compile('')($rootScope); - $rootScope.testUrl = 'http://example.com/image.png'; - $rootScope.$digest(); - expect(element.attr('src')).toEqual('http://example.com/image.png'); - // But it should accept trusted values anyway. - $rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.png'); - $rootScope.$digest(); - expect(element.attr('src')).toEqual('http://example.com/image2.png'); - })); - + ['img', 'audio', 'video'].forEach(function(tag) { // Support: IE 9 only - // IE9 rejects the video / audio tag with "Error: Not implemented" and the source tag with - // "Unable to get value of the property 'childNodes': object is null or undefined" - if (msie !== 9) { - they('should NOT require trusted values for $prop src', ['video', 'audio'], - function(tag) { - inject(function($rootScope, $compile, $sce) { + // 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) { element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); - $rootScope.testUrl = 'http://example.com/image.mp4'; + $rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted $rootScope.$digest(); expect(element.attr('src')).toEqual('http://example.com/image.mp4'); + })); - // But it should accept trusted values anyway. - $rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.mp4'); + it('should accept trusted values', inject(function($rootScope, $compile, $sce) { + // As a MEDIA_URL URL + element = $compile('<' + tag + ' src="{{testUrl}}">')($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.attr('src')).toEqual('http://example.com/image2.mp4'); + expect(element.attr('src')).toEqual('untrusted:foo()'); - // and trustedResourceUrls for retrocompatibility - $rootScope.testUrl = $sce.trustAsResourceUrl('http://example.com/image3.mp4'); + // As a URL + element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo()'); $rootScope.$digest(); - expect(element.attr('src')).toEqual('http://example.com/image3.mp4'); - }); + expect(element.attr('src')).toEqual('untrusted:foo()'); + + // As a RESOURCE URL + element = $compile('<' + tag + ' src="{{testUrl}}">')($rootScope); + $rootScope.testUrl = $sce.trustAsResourceUrl('untrusted:foo()'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('untrusted:foo()'); + })); }); + } + }); - they('should NOT require trusted values for $prop src', ['source', 'track'], - function(tag) { - inject(function($rootScope, $compile, $sce) { + // 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) { element = $compile('')($rootScope); - $rootScope.testUrl = 'http://example.com/image.mp4'; + $rootScope.testUrl = 'http://example.com/image.mp4'; // `http` is whitelisted $rootScope.$digest(); expect(element.find(tag).attr('src')).toEqual('http://example.com/image.mp4'); + })); - // But it should accept trusted values anyway. - $rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.mp4'); + it('should accept trusted values', inject(function($rootScope, $compile, $sce) { + // As a MEDIA_URL URL + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsMediaUrl('javascript:foo()'); $rootScope.$digest(); - expect(element.find(tag).attr('src')).toEqual('http://example.com/image2.mp4'); + expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); - // and trustedResourceUrls for retrocompatibility - $rootScope.testUrl = $sce.trustAsResourceUrl('http://example.com/image3.mp4'); + // As a URL + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('javascript:foo()'); $rootScope.$digest(); - expect(element.find(tag).attr('src')).toEqual('http://example.com/image3.mp4'); - }); + expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); + + // As a RESOURCE URL + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsResourceUrl('javascript:foo()'); + $rootScope.$digest(); + expect(element.find(tag).attr('src')).toEqual('javascript:foo()'); + })); }); - } - }); + }); + } describe('img[src] sanitization', function() { + it('should accept trusted values', inject(function($rootScope, $compile, $sce) { + element = $compile('')($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.attr('src')).toEqual('someUntrustedThing:foo();'); + })); + + it('should sanitize concatenated values even if they are trusted', inject(function($rootScope, $compile, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo();'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('unsafe:untrusted:foo();ponies'); + + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo();'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('http://untrusted:foo();'); + + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('untrusted:foo();'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('unsafe:untrusted:foo();untrusted:foo();'); + })); + it('should not sanitize attributes other than src', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'javascript:doEvilStuff()'; $rootScope.$apply(); - expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); - it('should use $$sanitizeUriProvider for reconfiguration of the src whitelist', function() { - module(function($compileProvider, $$sanitizeUriProvider) { - var newRe = /javascript:/, - returnVal; - expect($compileProvider.imgSrcSanitizationWhitelist()).toBe($$sanitizeUriProvider.imgSrcSanitizationWhitelist()); - - returnVal = $compileProvider.imgSrcSanitizationWhitelist(newRe); - expect(returnVal).toBe($compileProvider); - expect($$sanitizeUriProvider.imgSrcSanitizationWhitelist()).toBe(newRe); - expect($compileProvider.imgSrcSanitizationWhitelist()).toBe(newRe); - }); - inject(function() { - // needed to the module definition above is run... - }); - }); - it('should use $$sanitizeUri', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { @@ -11128,55 +11206,113 @@ describe('$compile', function() { expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true); }); }); + + + it('should use $$sanitizeUri on concatenated trusted values', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); + inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('javascript:foo();'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('someSanitizedUrl'); + + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('javascript:foo();'); + $rootScope.$digest(); + expect(element.attr('src')).toEqual('someSanitizedUrl'); + }); + }); + + 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) { + element = $compile('')($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.attr('src')).toEqual('untrusted:foo();'); + }); + }); }); describe('img[srcset] sanitization', function() { - - it('should not error if undefined', function() { + it('should not error if srcset is undefined', function() { var linked = false; module(function() { directive('setter', valueFn(function(scope, elem, attrs) { + // Set srcset to a value attrs.$set('srcset', 'http://example.com/'); expect(attrs.srcset).toBe('http://example.com/'); - + // Now set it to undefined attrs.$set('srcset', undefined); expect(attrs.srcset).toBeUndefined(); - linked = true; })); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); - expect(linked).toBe(true); expect(element.attr('srcset')).toBeUndefined(); }); }); - it('should NOT require trusted values for img srcset', inject(function($rootScope, $compile, $sce) { + it('should NOT require trusted values for whitelisted values', inject(function($rootScope, $compile, $sce) { element = $compile('')($rootScope); - $rootScope.testUrl = 'http://example.com/image.png'; + $rootScope.testUrl = 'http://example.com/image.png'; // `http` is whitelisted $rootScope.$digest(); expect(element.attr('srcset')).toEqual('http://example.com/image.png'); - // But it should accept trusted values anyway. - $rootScope.testUrl = $sce.trustAsUrl('http://example.com/image2.png'); + })); + + it('should accept trusted values, if they are also whitelisted', inject(function($rootScope, $compile, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('http://example.com'); + $rootScope.$digest(); + expect(element.attr('srcset')).toEqual('http://example.com'); + })); + + it('does 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); + $rootScope.testUrl = $sce.trustAsUrl('javascript:something'); + $rootScope.$digest(); + expect(element.attr('srcset')).toEqual('unsafe:javascript:something'); + + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl('javascript:something'); $rootScope.$digest(); - expect(element.attr('srcset')).toEqual('http://example.com/image2.png'); + expect(element.attr('srcset')).toEqual( + 'unsafe:javascript:something ,unsafe:javascript:something'); })); it('should use $$sanitizeUri', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'someUrl'; - - $$sanitizeUri.and.returnValue('someSanitizedUrl'); $rootScope.$apply(); expect(element.attr('srcset')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true); + + element = $compile('')($rootScope); + $rootScope.testUrl = 'javascript:yay'; + $rootScope.$apply(); + expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl'); + + element = $compile('')($rootScope); + $rootScope.testUrl = 'script:yay, javascript:nay'; + $rootScope.$apply(); + expect(element.attr('srcset')).toEqual('someSanitizedUrl ,someSanitizedUrl'); }); }); @@ -11220,6 +11356,38 @@ describe('$compile', function() { }); 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 + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('href')).toEqual('http://example.com/image.png'); + + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('ng-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 + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('href')).toEqual('javascript:foo()'); + + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('ng-href')).toEqual('javascript:foo()'); + })); + + it('should sanitize non-whitelisted values', inject(function($rootScope, $compile) { + $rootScope.testUrl = 'javascript:foo()'; // `javascript` is not whitelisted + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('href')).toEqual('unsafe:javascript:foo()'); + + element = $compile('')($rootScope); + $rootScope.$digest(); + expect(element.attr('href')).toEqual('unsafe:javascript:foo()'); + })); it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) { element = $compile('
')($rootScope); @@ -11229,7 +11397,7 @@ describe('$compile', function() { expect(element.attr('href')).toBe('javascript:doEvilStuff()'); })); - it('should not sanitize attributes other than href', inject(function($compile, $rootScope) { + it('should not sanitize attributes other than href/ng-href', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'javascript:doEvilStuff()'; $rootScope.$apply(); @@ -11237,48 +11405,21 @@ describe('$compile', function() { expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); - it('should use $$sanitizeUriProvider for reconfiguration of the href whitelist', function() { - module(function($compileProvider, $$sanitizeUriProvider) { - var newRe = /javascript:/, - returnVal; - expect($compileProvider.aHrefSanitizationWhitelist()).toBe($$sanitizeUriProvider.aHrefSanitizationWhitelist()); - - returnVal = $compileProvider.aHrefSanitizationWhitelist(newRe); - expect(returnVal).toBe($compileProvider); - expect($$sanitizeUriProvider.aHrefSanitizationWhitelist()).toBe(newRe); - expect($compileProvider.aHrefSanitizationWhitelist()).toBe(newRe); - }); - inject(function() { - // needed to the module definition above is run... - }); - }); - it('should use $$sanitizeUri', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'someUrl'; - - $$sanitizeUri.and.returnValue('someSanitizedUrl'); $rootScope.$apply(); expect(element.attr('href')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); - }); - }); - it('should use $$sanitizeUri when declared via ng-href', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); - module(function($provide) { - $provide.value('$$sanitizeUri', $$sanitizeUri); - }); - inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = 'someUrl'; + $$sanitizeUri.calls.reset(); - $$sanitizeUri.and.returnValue('someSanitizedUrl'); + element = $compile('')($rootScope); $rootScope.$apply(); expect(element.attr('href')).toBe('someSanitizedUrl'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); @@ -11286,72 +11427,72 @@ describe('$compile', function() { }); it('should use $$sanitizeUri when working with svg and xlink:href', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { - var elementA = $compile('')($rootScope); - var elementImage = $compile('')($rootScope); - - //both of these fail the RESOURCE_URL test, that shouldn't be run + // This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run + // because these interpolations will be resolved against the URL context instead $rootScope.testUrl = 'https://bad.example.org'; - $$sanitizeUri.and.returnValue('https://clean.example.org'); + var elementA = $compile('')($rootScope); $rootScope.$apply(); expect(elementA.find('a').attr('xlink:href')).toBe('https://clean.example.org'); + expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'aTag', false); + + var elementImage = $compile('')($rootScope); + $rootScope.$apply(); expect(elementImage.find('image').attr('xlink:href')).toBe('https://clean.example.org'); - // is navigational, so the second argument should be false to reach the aHref whitelist - expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'aTag' , false); - // is media inclusion, it should use the imgSrc whitelist expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl + 'imageTag', true); }); }); it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('https://clean.example.org'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); }); inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - //both of these fail the RESOURCE_URL test, that shouldn't be run + // This URL would fail the RESOURCE_URL whitelist, but that test shouldn't be run + // because these interpolations will be resolved against the URL context instead $rootScope.testUrl = 'https://bad.example.org'; - $$sanitizeUri.and.returnValue('https://clean.example.org'); - $rootScope.$apply(); - expect(element.find('a').prop('href').baseVal).toBe('https://clean.example.org'); - expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); - }); - }); - - - it('should use $$sanitizeUri when working with svg and xlink:href through ng-href', function() { - var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); - module(function($provide) { - $provide.value('$$sanitizeUri', $$sanitizeUri); - }); - inject(function($compile, $rootScope) { element = $compile('')($rootScope); - $rootScope.testUrl = 'evilUrl'; - - $$sanitizeUri.and.returnValue('someSanitizedUrl'); $rootScope.$apply(); - expect(element.find('a').prop('href').baseVal).toBe('someSanitizedUrl'); + expect(element.find('a').prop('href').baseVal).toBe('https://clean.example.org'); expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); - it('should have a RESOURCE_URL context for xlink:href by default', function() { + it('should require a RESOURCE_URL context for xlink:href by if not on an anchor or image', function() { inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = 'https://bad.example.org'; expect(function() { $rootScope.$apply(); - }).toThrowError(/\$sce:insecurl/); + }).toThrowMinErr('$interpolate', 'interr', 'Can\'t interpolate: {{ testUrl }}\n' + + 'Error: [$sce:insecurl] Blocked loading resource from url not allowed by $sceDelegate policy. ' + + 'URL: https://bad.example.org'); }); }); + + it('should not have endless digests when given arrays in concatenable context', inject(function($compile, $rootScope) { + element = $compile('' + + '')($rootScope); + $rootScope.testUrl = [1]; + $rootScope.$digest(); + + $rootScope.testUrl = []; + $rootScope.$digest(); + + $rootScope.testUrl = {a:'b'}; + $rootScope.$digest(); + + $rootScope.testUrl = {}; + $rootScope.$digest(); + })); }); describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() { diff --git a/test/ng/directive/booleanAttrsSpec.js b/test/ng/directive/booleanAttrsSpec.js index ac6cbdcbfd04..f1e79ee61673 100644 --- a/test/ng/directive/booleanAttrsSpec.js +++ b/test/ng/directive/booleanAttrsSpec.js @@ -123,7 +123,7 @@ describe('boolean attr directives', function() { describe('ngSrc', function() { it('should interpolate the expression and bind to src with raw same-domain value', inject(function($compile, $rootScope) { - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('src')).toBeUndefined(); @@ -138,7 +138,7 @@ describe('ngSrc', function() { it('should interpolate the expression and bind to src with a trusted value', inject(function($compile, $rootScope, $sce) { - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('src')).toBeUndefined(); @@ -152,9 +152,12 @@ describe('ngSrc', function() { })); - it('should NOT interpolate a multi-part expression for non-img src attribute', inject(function($compile, $rootScope) { + it('should NOT interpolate a multi-part expression for non-URL context src attribute', inject(function($compile, $rootScope) { expect(function() { - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); + $rootScope.$apply(function() { + $rootScope.id = 1; + }); dealoc(element); }).toThrowMinErr( '$interpolate', 'noconcat', 'Error while interpolating: some/{{id}}\nStrict ' + @@ -162,6 +165,15 @@ describe('ngSrc', function() { 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); })); + it('should interpolate a multi-part expression for img src attribute (URL context)', inject(function($compile, $rootScope) { + var element = $compile('')($rootScope); + expect(element.attr('src')).toBe(undefined); // URL concatenations are all-or-nothing + $rootScope.$apply(function() { + $rootScope.id = 1; + }); + expect(element.attr('src')).toEqual('some/1'); + })); + it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) { var element = $compile('
')($rootScope); @@ -176,7 +188,7 @@ describe('ngSrc', function() { it('should NOT interpolate a wrongly typed expression', inject(function($compile, $rootScope, $sce) { expect(function() { - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); $rootScope.$apply(function() { $rootScope.id = $sce.trustAsUrl('http://somewhere'); }); @@ -195,23 +207,23 @@ describe('ngSrc', function() { // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); $rootScope.$digest(); - expect(element.prop('src')).toBeUndefined(); + expect(element.prop('src')).toBe(''); dealoc(element); - element = $compile('
')($rootScope); + element = $compile('')($rootScope); $rootScope.$digest(); - expect(element.prop('src')).toEqual('some/'); + expect(element.prop('src')).toContain('some/'); dealoc(element); - element = $compile('
')($rootScope); + element = $compile('')($rootScope); $rootScope.$apply(function() { $rootScope.id = $sce.trustAsResourceUrl('http://somewhere'); }); - expect(element.prop('src')).toEqual('http://somewhere'); + expect(element.prop('src')).toEqual('http://somewhere/'); dealoc(element); })); @@ -221,7 +233,7 @@ describe('ngSrc', function() { describe('ngSrcset', function() { it('should interpolate the expression and bind to srcset', inject(function($compile, $rootScope) { - var element = $compile('
')($rootScope); + var element = $compile('')($rootScope); $rootScope.$digest(); expect(element.attr('srcset')).toBeUndefined(); @@ -245,7 +257,7 @@ describe('ngHref', function() { it('should interpolate the expression and bind to href', inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.attr('href')).toEqual('some/'); @@ -294,9 +306,8 @@ describe('ngHref', function() { // IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence // See https://github.com/angular/angular.js/issues/13388 it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) { - element = $compile('')($rootScope); - expect(function() { + element = $compile('')($rootScope); $rootScope.$digest(); }).toThrow(); })); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 2ed9b31b7f5f..3f31c98dcc26 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -1,5 +1,7 @@ 'use strict'; +/* eslint-disable no-script-url */ + describe('$interpolate', function() { it('should return the interpolation object when there are no bindings and textOnly is undefined', @@ -267,7 +269,9 @@ describe('$interpolate', function() { expect(function() { $interpolate('{{foo}}', true, sce.CSS)(scope); - }).toThrowMinErr('$interpolate', 'interr'); + }).toThrowMinErr( + '$interpolate', 'interr', 'Can\'t interpolate: {{foo}}\nError: [$sce:unsafe] ' + + 'Attempting to use an unsafe value in a safe context.'); })); it('should NOT interpolate mistyped expressions', inject(function($interpolate, $rootScope) { @@ -276,7 +280,9 @@ describe('$interpolate', function() { expect(function() { $interpolate('{{foo}}', true, sce.HTML)(scope); - }).toThrowMinErr('$interpolate', 'interr'); + }).toThrowMinErr( + '$interpolate', 'interr', 'Can\'t interpolate: {{foo}}\nError: [$sce:unsafe] ' + + 'Attempting to use an unsafe value in a safe context.'); })); it('should interpolate trusted expressions in a regular context', inject(function($interpolate) { @@ -291,17 +297,16 @@ describe('$interpolate', function() { // The concatenation of trusted values does not necessarily result in a trusted value. (For // instance, you can construct evil JS code by putting together pieces of JS strings that are by - // themselves safe to execute in isolation.) + // themselves safe to execute in isolation). Therefore, some contexts disable it, such as CSS. it('should NOT interpolate trusted expressions with multiple parts', inject(function($interpolate) { var foo = sce.trustAsCss('foo'); var bar = sce.trustAsCss('bar'); expect(function() { return $interpolate('{{foo}}{{bar}}', true, sce.CSS)({foo: foo, bar: bar}); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{foo}}{{bar}}\n' + + '$interpolate', 'interr', 'Error while interpolating: {{foo}}{{bar}}\n' + 'Strict Contextual Escaping disallows interpolations that concatenate multiple ' + - 'expressions when a trusted value is required. See ' + - 'http://docs.angularjs.org/api/ng.$sce'); + 'expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); })); }); @@ -380,26 +385,32 @@ describe('$interpolate', function() { describe('isTrustedContext', function() { - it('should NOT interpolate a multi-part expression when isTrustedContext is true', inject(function($interpolate) { - var isTrustedContext = true; + it('should NOT interpolate a multi-part expression when isTrustedContext is RESOURCE_URL', inject(function($sce, $interpolate) { + var isTrustedContext = $sce.RESOURCE_URL; expect(function() { - $interpolate('constant/{{var}}', true, isTrustedContext); + $interpolate('constant/{{var}}', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: constant/{{var}}\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); + '$interpolate', 'interr', + 'Can\'t interpolate: constant/{{var}}\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: constant/{{var}}\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); expect(function() { - $interpolate('{{var}}/constant', true, isTrustedContext); + $interpolate('{{var}}/constant', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{var}}/constant\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); - expect(function() { - $interpolate('{{foo}}{{bar}}', true, isTrustedContext); + '$interpolate', 'interr', + 'Can\'t interpolate: {{var}}/constant\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: {{var}}/constant\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); + expect(function() { + $interpolate('{{foo}}{{bar}}', true, isTrustedContext)('val'); }).toThrowMinErr( - '$interpolate', 'noconcat', 'Error while interpolating: {{foo}}{{bar}}\nStrict ' + - 'Contextual Escaping disallows interpolations that concatenate multiple expressions ' + - 'when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce'); + '$interpolate', 'interr', + 'Can\'t interpolate: {{foo}}{{bar}}\nError: [$interpolate:noconcat] Error while ' + + 'interpolating: {{foo}}{{bar}}\nStrict Contextual Escaping disallows interpolations ' + + 'that concatenate multiple expressions when a trusted value is required. ' + + 'See http://docs.angularjs.org/api/ng.$sce'); })); it('should interpolate a multi-part expression when isTrustedContext is false', inject(function($interpolate) { @@ -407,6 +418,24 @@ describe('$interpolate', function() { expect($interpolate('some/{{id}}')({id: 1})).toEqual('some/1'); expect($interpolate('{{foo}}{{bar}}')({foo: 1, bar: 2})).toEqual('12'); })); + + + it('should interpolate a multi-part expression when isTrustedContext is URL', inject(function($sce, $interpolate) { + expect($interpolate('some/{{id}}', true, $sce.URL)({})).toEqual('some/'); + expect($interpolate('some/{{id}}', true, $sce.URL)({id: 1})).toEqual('some/1'); + expect($interpolate('{{foo}}{{bar}}', true, $sce.URL)({foo: 1, bar: 2})).toEqual('12'); + })); + + + it('should interpolate and sanitize a multi-part expression when isTrustedContext is URL', inject(function($sce, $interpolate) { + /* jshint scripturl:true */ + expect($interpolate('some/{{id}}', true, $sce.URL)({})).toEqual('some/'); + expect($interpolate('some/{{id}}', true, $sce.URL)({id: 'javascript:'})).toEqual('some/javascript:'); + expect($interpolate('{{foo}}{{bar}}', true, $sce.URL)({foo: 'javascript:', bar: 'javascript:'})).toEqual('unsafe:javascript:javascript:'); + })); + + + }); diff --git a/test/ng/sceSpecs.js b/test/ng/sceSpecs.js index f7c654df296a..fb169925c9ff 100644 --- a/test/ng/sceSpecs.js +++ b/test/ng/sceSpecs.js @@ -1,5 +1,7 @@ 'use strict'; +/* eslint-disable no-script-url */ + describe('SCE', function() { describe('when disabled', function() { @@ -211,7 +213,7 @@ describe('SCE', function() { expect($sce.parseAsJs('"string"')()).toBe('string'); })); - it('should be possible to do one-time binding', function() { + it('should be possible to do one-time binding on a non-concatenable context', function() { module(provideLog); inject(function($sce, $rootScope, log) { $rootScope.$watch($sce.parseAsHtml('::foo'), function(value) { @@ -236,6 +238,31 @@ describe('SCE', function() { }); }); + it('should be possible to do one-time binding on a concatenable context', function() { + module(provideLog); + inject(function($sce, $rootScope, log) { + $rootScope.$watch($sce.parseAsUrl('::foo'), function(value) { + log(value + ''); + }); + + $rootScope.$digest(); + expect(log).toEqual('undefined'); // initial listener call + log.reset(); + + $rootScope.foo = $sce.trustAs($sce.URL, 'trustedValue'); + expect($rootScope.$$watchers.length).toBe(1); + $rootScope.$digest(); + + expect($rootScope.$$watchers.length).toBe(0); + expect(log).toEqual('trustedValue'); + log.reset(); + + $rootScope.foo = $sce.trustAs($sce.URL, 'anotherTrustedValue'); + $rootScope.$digest(); + expect(log).toEqual(''); // watcher no longer active + }); + }); + it('should NOT parse constant non-literals', inject(function($sce) { // Until there's a real world use case for this, we're disallowing // constant non-literals. See $SceParseProvider. @@ -525,6 +552,44 @@ describe('SCE', function() { )); }); + describe('URL-context sanitization', function() { + it('should sanitize values that are not whitelisted', inject(function($sce) { + expect($sce.getTrustedMediaUrl('javascript:foo')).toEqual('unsafe:javascript:foo'); + expect($sce.getTrustedUrl('javascript:foo')).toEqual('unsafe:javascript:foo'); + })); + + it('should not sanitize values that are whitelisted', inject(function($sce) { + expect($sce.getTrustedMediaUrl('http://example.com')).toEqual('http://example.com'); + expect($sce.getTrustedUrl('http://example.com')).toEqual('http://example.com'); + })); + + it('should not sanitize trusted values', inject(function($sce) { + expect($sce.getTrustedMediaUrl($sce.trustAsMediaUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedMediaUrl($sce.trustAsUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedMediaUrl($sce.trustAsResourceUrl('javascript:foo'))).toEqual('javascript:foo'); + + expect($sce.getTrustedUrl($sce.trustAsMediaUrl('javascript:foo'))).toEqual('unsafe:javascript:foo'); + expect($sce.getTrustedUrl($sce.trustAsUrl('javascript:foo'))).toEqual('javascript:foo'); + expect($sce.getTrustedUrl($sce.trustAsResourceUrl('javascript:foo'))).toEqual('javascript:foo'); + })); + + it('should use the $$sanitizeUri', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri').and.returnValue('someSanitizedUrl'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); + inject(function($sce) { + expect($sce.getTrustedMediaUrl('someUrl')).toEqual('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledOnceWith('someUrl', true); + + $$sanitizeUri.calls.reset(); + + expect($sce.getTrustedUrl('someUrl')).toEqual('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledOnceWith('someUrl', false); + }); + }); + }); + describe('sanitizing html', function() { describe('when $sanitize is NOT available', function() { it('should throw an exception for getTrusted(string) values', inject(function($sce) { @@ -535,9 +600,23 @@ describe('SCE', function() { describe('when $sanitize is available', function() { beforeEach(function() { module('ngSanitize'); }); + it('should sanitize html using $sanitize', inject(function($sce) { expect($sce.getTrustedHtml('abc')).toBe('abc'); })); + + // Note: that test only passes if HTML is added to the concatenable contexts list. + // See isConcatenableSecureContext in interpolate.js for that. + // + // if (!msie || msie >= 11) { + // it('can set dynamic srcdocs with concatenations and sanitize the result', + // inject(function($compile, $rootScope) { + // var element = $compile('')($rootScope); + // $rootScope.html = 'noyes'; + // $rootScope.$digest(); + // expect(angular.lowercase(element.attr('srcdoc'))).toEqual('yes'); + // })); + // } }); }); }); diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index 2bab68093181..a8a79c5d861b 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -473,7 +473,7 @@ describe('HTML', function() { }); }); - it('should use $$sanitizeUri for links', function() { + it('should use $$sanitizeUri for a[href] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri); @@ -489,7 +489,7 @@ describe('HTML', function() { }); }); - it('should use $$sanitizeUri for links', function() { + it('should use $$sanitizeUri for img[src] links', function() { var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); module(function($provide) { $provide.value('$$sanitizeUri', $$sanitizeUri);