From bea9422ebfc8e80ee28ad81afc62d2e432c85cbb Mon Sep 17 00:00:00 2001 From: Chirayu Krishnappa Date: Tue, 14 May 2013 14:51:39 -0700 Subject: [PATCH] feat($sce): new $sce service for Strict Contextual Escaping. $sce is a service that provides Strict Contextual Escaping services to AngularJS. Strict Contextual Escaping -------------------------- Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context One example of such a context is binding arbitrary html controlled by the user via ng-bind-html-unsafe. We refer to these contexts as privileged or SCE contexts. As of version 1.2, Angular ships with SCE enabled by default. Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows one to execute arbitrary javascript by the use of the expression() syntax. Refer http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx to learn more about them. You can ensure your document is in standards mode and not quirks mode by adding to the top of your HTML document. SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. Here's an example of a binding in a privileged context:
Notice that ng-bind-html-unsafe is bound to {{userHtml}} controlled by the user. With SCE disabled, this application allows the user to render arbitrary HTML into the DIV. In a more realistic example, one may be rendering user comments, blog articles, etc. via bindings. (HTML is just one example of a context where rendering user controlled input creates security vulnerabilities.) For the case of HTML, you might use a library, either on the client side, or on the server side, to sanitize unsafe HTML before binding to the value and rendering it in the document. How would you ensure that every place that used these types of bindings was bound to a value that was sanitized by your library (or returned as safe for rendering by your server?) How can you ensure that you didn't accidentally delete the line that sanitized the value, or renamed some properties/fields and forgot to update the binding to the sanitized value? To be secure by default, you want to ensure that any such bindings are disallowed unless you can determine that something explicitly says it's safe to use a value for binding in that context. You can then audit your code (a simple grep would do) to ensure that this is only done for those values that you can easily tell are safe - because they were received from your server, sanitized by your library, etc. You can organize your codebase to help with this - perhaps allowing only the files in a specific directory to do this. Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. In the case of AngularJS' SCE service, one uses $sce.trustAs (and shorthand methods such as $sce.trustAsHtml, etc.) to obtain values that will be accepted by SCE / privileged contexts. In privileged contexts, directives and code will bind to the result of $sce.getTrusted(context, value) rather than to the value directly. Directives use $sce.parseAs rather than $parse to watch attribute bindings, which performs the $sce.getTrusted behind the scenes on non-constant literals. As an example, ngBindHtmlUnsafe uses $sce.parseAsHtml(binding expression). Here's the actual code (slightly simplified): var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) { element.html(value || ''); }); }; }]; Impact on loading templates --------------------------- This applies both to the ng-include directive as well as templateUrl's specified by directives. By default, Angular only loads templates from the same domain and protocol as the application document. This is done by calling $sce.getTrustedResourceUrl on the template URL. To load templates from other domains and/or protocols, you may either either whitelist them or wrap it into a trusted value. *Please note*: The browser's Same Origin Policy and Cross-Origin Resource Sharing (CORS) policy apply in addition to this and may further restrict whether the template is successfully loaded. This means that without the right CORS policy, loading templates from a different domain won't work on all browsers. Also, loading templates from file:// URL does not work on some browsers. This feels like too much overhead for the developer? ---------------------------------------------------- It's important to remember that SCE only applies to interpolation expressions. 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. Additionally, a[href] and img[src] automatically sanitize their URLs and do not pass them through $sce.getTrusted. SCE doesn't play a role here. The included $sceDelegate comes with sane defaults to allow you to load templates in ng-include from your application's domain without having to even know about SCE. It blocks loading templates from other domains or loading templates over http from an https served document. You can change these by setting your own custom whitelists and blacklists for matching such URLs. This significantly reduces the overhead. It is far easier to pay the small overhead and have an application that's secure and can be audited to verify that with much more ease than bolting security onto an application later. --- angularFiles.js | 9 +- docs/content/error/sce/icontext.ngdoc | 6 + docs/content/error/sce/iequirks.ngdoc | 16 + docs/content/error/sce/isecrurl.ngdoc | 30 + docs/content/error/sce/itype.ngdoc | 6 + docs/content/error/sce/unsafe.ngdoc | 15 + docs/content/guide/directive.ngdoc | 7 +- docs/src/example.js | 3 + docs/src/templates/js/docs.js | 3 +- src/AngularPublic.js | 2 + src/ng/compile.js | 12 +- src/ng/directive/ngBind.js | 4 +- src/ng/directive/ngInclude.js | 23 +- src/ng/interpolate.js | 27 +- src/ng/sce.js | 959 ++++++++++++++++++++++++++ src/ng/urlUtils.js | 5 +- src/ngRoute/route.js | 25 +- src/ngSanitize/sanitize.js | 14 +- test/ng/compileSpec.js | 90 ++- test/ng/directive/booleanAttrsSpec.js | 104 ++- test/ng/directive/ngBindSpec.js | 48 +- test/ng/directive/ngIncludeSpec.js | 50 +- test/ng/directive/ngSrcSpec.js | 44 ++ test/ng/interpolateSpec.js | 59 +- test/ng/sceSpecs.js | 347 ++++++++++ test/ng/urlUtilsSpec.js | 18 +- test/ngRoute/routeSpec.js | 30 +- test/testabilityPatch.js | 7 +- 28 files changed, 1853 insertions(+), 110 deletions(-) create mode 100644 docs/content/error/sce/icontext.ngdoc create mode 100644 docs/content/error/sce/iequirks.ngdoc create mode 100644 docs/content/error/sce/isecrurl.ngdoc create mode 100644 docs/content/error/sce/itype.ngdoc create mode 100644 docs/content/error/sce/unsafe.ngdoc create mode 100644 src/ng/sce.js create mode 100644 test/ng/sceSpecs.js diff --git a/angularFiles.js b/angularFiles.js index b93283a7cc21..694bde47c0a9 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -18,19 +18,20 @@ angularFiles = { 'src/ng/controller.js', 'src/ng/document.js', 'src/ng/exceptionHandler.js', + 'src/ng/http.js', + 'src/ng/httpBackend.js', 'src/ng/interpolate.js', + 'src/ng/locale.js', 'src/ng/location.js', 'src/ng/log.js', 'src/ng/parse.js', 'src/ng/q.js', 'src/ng/rootScope.js', + 'src/ng/sce.js', 'src/ng/sniffer.js', - 'src/ng/window.js', - 'src/ng/http.js', - 'src/ng/httpBackend.js', - 'src/ng/locale.js', 'src/ng/timeout.js', 'src/ng/urlUtils.js', + 'src/ng/window.js', 'src/ng/filter.js', 'src/ng/filter/filter.js', diff --git a/docs/content/error/sce/icontext.ngdoc b/docs/content/error/sce/icontext.ngdoc new file mode 100644 index 000000000000..af629040ff27 --- /dev/null +++ b/docs/content/error/sce/icontext.ngdoc @@ -0,0 +1,6 @@ +@ngdoc error +@name $sce:icontext +@fullName Invalid / Unknown SCE context +@description +The context enum passed to {@link api/ng.$sce#trustAs $sce.trustAs} was not recognized. Refer the +list of {@link api/ng.$sce#contexts supported Strict Contextual Escaping (SCE) contexts}. diff --git a/docs/content/error/sce/iequirks.ngdoc b/docs/content/error/sce/iequirks.ngdoc new file mode 100644 index 000000000000..be87334484cc --- /dev/null +++ b/docs/content/error/sce/iequirks.ngdoc @@ -0,0 +1,16 @@ +@ngdoc error +@name $sce:iequirks +@fullName IE8 in quirks mode is unsupported. +@description +You are using AngularJS with {@link api/ng.$sce#strictcontextualescaping Strict Contextual Escaping +(SCE)} mode enabled (the default) on IE8 or lower in quirks mode. In this mode, IE8 allows one to +execute arbitrary javascript by the use of the `expression()` syntax and is not supported. Refer +{@link http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx MSDN Blogs > IEBlog > +Ending Expressions} to learn more about them. + +### Recommended solution +Add the doctype + + + +to the top of your HTML document. This switches the document from quirks mode to standards mode. diff --git a/docs/content/error/sce/isecrurl.ngdoc b/docs/content/error/sce/isecrurl.ngdoc new file mode 100644 index 000000000000..c5a5445d484f --- /dev/null +++ b/docs/content/error/sce/isecrurl.ngdoc @@ -0,0 +1,30 @@ +@ngdoc error +@name $sce:isecrurl +@fullName Blocked loading an untrusted resource +@description + +AngularJS' {@link api/ng.$sce#strictcontextualescaping Strict Contextual Escaping +(SCE)} mode (enabled by default) has blocked loading a resource from an insecure URL. + +Typically, this would occur if you're attempting to load an Angular template from a different +domain. It's also possible that a custom directive threw this error for a similar reason. + +Angular only loads templates from trusted URLs (by calling {@link api/ng.$sce#getTrustedResourceUrl +$sce.getTrustedResourceUrl} on the template URL.). + +By default, only URLs to the same domain with the same protocol as the application document are +considered to be trusted. + +The {@link api/ng.directive:ngInclude ng-include} directive and {@link guide/directive directives} +that specify a `templateUrl` require a trusted resource URL. + +To load templates from other domains and/or protocols, either adjust the {@link +api/ng.$sceDelegateProvider#resourceUrlWhitelist whitelist}/ {@link +api/ng.$sceDelegateProvider#resourceUrlBlacklist blacklist} or wrap the URL with a call to {@link +api/ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl}. + +**Note**: The browser's {@link +https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest Same Origin +Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing (CORS)} policy apply +that may further restrict whether the template is successfully loaded. (e.g. neither cross-domain +requests won't work on all browsers nor `file://` requests on some browsers) diff --git a/docs/content/error/sce/itype.ngdoc b/docs/content/error/sce/itype.ngdoc new file mode 100644 index 000000000000..90555156082b --- /dev/null +++ b/docs/content/error/sce/itype.ngdoc @@ -0,0 +1,6 @@ +@ngdoc error +@name $sce:itype +@fullName String value required for SCE trust call. +@description +{@link api/ng.$sce#trustAs $sce.trustAs} requires a string value. Read more about {@link +api/ng.$sce#strictcontextualescaping Strict Contextual Escaping (SCE)} in AngularJS. diff --git a/docs/content/error/sce/unsafe.ngdoc b/docs/content/error/sce/unsafe.ngdoc new file mode 100644 index 000000000000..908033d2b39d --- /dev/null +++ b/docs/content/error/sce/unsafe.ngdoc @@ -0,0 +1,15 @@ +@ngdoc error +@name $sce:unsafe +@fullName Require a safe/trusted value +@description + +The value provided for use in a specific context was not found to be safe/trusted for use. + +Angular's {@link api/ng.$sce#strictcontextualescaping Strict Contextual Escaping (SCE)} mode +(enabled by default), requires bindings in certain +contexts to result in a value that is trusted as safe for use in such a context. (e.g. loading an +Angular template from a URL requires that the URL is one considered safe for loading resources.) + +This helps prevent XSS and other security issues. Read more at {@link +api/ng.$sce#strictcontextualescaping Strict Contextual Escaping (SCE)} + diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 73c7ead757dd..5e682d589d48 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -415,8 +415,8 @@ compiler}. The attributes are: {@link guide/directive#Components Creating Components} section below for more information. You can specify `template` as a string representing the template or as a function which takes - two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns - a string value representing the template. + two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and + returns a string value representing the template. * `templateUrl` - Same as `template` but the template is loaded from the specified URL. Because the template loading is asynchronous the compilation/linking is suspended until the template @@ -424,7 +424,8 @@ compiler}. The attributes are: You can specify `templateUrl` as a string representing the URL or as a function which takes two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns - a string value representing the url. + a string value representing the url. In either case, the template URL is passed through {@link + api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * `replace` - if set to `true` then the template will replace the current element, rather than append the template to the element. diff --git a/docs/src/example.js b/docs/src/example.js index eadc218f1f35..cdbc24a7c2f8 100644 --- a/docs/src/example.js +++ b/docs/src/example.js @@ -20,6 +20,7 @@ exports.Example = function(scenarios) { this.html = []; this.css = []; this.js = []; + this.json = []; this.unit = []; this.scenario = []; this.scenarios = scenarios; @@ -88,6 +89,7 @@ exports.Example.prototype.toHtmlEdit = function() { out.push(' source-edit-html="' + ids(this.html) + '"'); out.push(' source-edit-css="' + ids(this.css) + '"'); out.push(' source-edit-js="' + ids(this.js) + '"'); + out.push(' source-edit-json="' + ids(this.json) + '"'); out.push(' source-edit-unit="' + ids(this.unit) + '"'); out.push(' source-edit-scenario="' + ids(this.scenario) + '"'); out.push('>
\n'); @@ -102,6 +104,7 @@ exports.Example.prototype.toHtmlTabs = function() { htmlTabs(this.html); htmlTabs(this.css); htmlTabs(this.js); + htmlTabs(this.json); htmlTabs(this.unit); htmlTabs(this.scenario); out.push(''); diff --git a/docs/src/templates/js/docs.js b/docs/src/templates/js/docs.js index a14237fa459d..7cac6a9a30a3 100644 --- a/docs/src/templates/js/docs.js +++ b/docs/src/templates/js/docs.js @@ -216,6 +216,7 @@ docsApp.directive.sourceEdit = function(getEmbeddedTemplate) { html: read($attrs.sourceEditHtml), css: read($attrs.sourceEditCss), js: read($attrs.sourceEditJs), + json: read($attrs.sourceEditJson), unit: read($attrs.sourceEditUnit), scenario: read($attrs.sourceEditScenario) }; @@ -358,7 +359,7 @@ docsApp.serviceFactory.formPostData = function($document) { docsApp.serviceFactory.openPlunkr = function(templateMerge, formPostData, angularUrls) { return function(content) { - var allFiles = [].concat(content.js, content.css, content.html); + var allFiles = [].concat(content.js, content.css, content.html, content.json); var indexHtmlContent = '\n' + '\n' + ' \n' + diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 99f6cdbd4cad..25e21b2c5b4e 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -122,6 +122,8 @@ function publishExternalAPI(angular){ $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $sce: $SceProvider, + $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, diff --git a/src/ng/compile.js b/src/ng/compile.js index c059af47d3dd..8297682234d0 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -274,9 +274,9 @@ function $CompileProvider($provide) { this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$$urlUtils', + '$controller', '$rootScope', '$document', '$sce', '$$urlUtils', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $$urlUtils) { + $controller, $rootScope, $document, $sce, $$urlUtils) { var Attributes = function(element, attr) { this.$$element = element; @@ -1095,7 +1095,7 @@ function $CompileProvider($provide) { $compileNode.html(''); - $http.get(templateUrl, {cache: $templateCache}). + $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). success(function(content) { var compileNode, tempTemplateAttrs, $template; @@ -1203,12 +1203,12 @@ function $CompileProvider($provide) { } - function isTrustedContext(node, attrNormalizedName) { + function getTrustedContext(node, attrNormalizedName) { // maction[xlink:href] can source SVG. It's not limited to . if (attrNormalizedName == "xlinkHref" || (nodeName_(node) != "IMG" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { - return true; + return $sce.RESOURCE_URL; } } @@ -1238,7 +1238,7 @@ function $CompileProvider($provide) { // we need to interpolate again, in case the attribute value has been updated // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, isTrustedContext(node, name)); + interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); // if attribute was updated so that there is no interpolation going on we don't want to // register any observers diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index f1cf4c70e480..fc54adcf0bc2 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -129,10 +129,10 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @element ANY * @param {expression} ngBindHtmlUnsafe {@link guide/expression Expression} to evaluate. */ -var ngBindHtmlUnsafeDirective = [function() { +var ngBindHtmlUnsafeDirective = ['$sce', function($sce) { return function(scope, element, attr) { element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe); - scope.$watch(attr.ngBindHtmlUnsafe, function ngBindHtmlUnsafeWatchAction(value) { + scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function ngBindHtmlUnsafeWatchAction(value) { element.html(value || ''); }); }; diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js index adcc46e55c07..72b5af08a5ca 100644 --- a/src/ng/directive/ngInclude.js +++ b/src/ng/directive/ngInclude.js @@ -8,9 +8,20 @@ * @description * Fetches, compiles and includes an external HTML fragment. * - * Keep in mind that Same Origin Policy applies to included resources - * (e.g. ngInclude won't work for cross-domain requests on all browsers and for - * file:// access on some browsers). + * Keep in mind that: + * + * - by default, the template URL is restricted to the same domain and protocol as the + * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on it. To load templates from other domains and/or protocols, + * you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or + * {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. Refer Angular's {@link + * ng.$sce Strict Contextual Escaping}. + * - in addition, the browser's + * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest + * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing + * (CORS)} policy apply that may further restrict whether the template is successfully loaded. + * (e.g. ngInclude won't work for cross-domain requests on all browsers and for `file://` + * access on some browsers) * * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter** * and **leave** effects. @@ -132,8 +143,8 @@ * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator', - function($http, $templateCache, $anchorScroll, $compile, $animator) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator', '$sce', + function($http, $templateCache, $anchorScroll, $compile, $animator, $sce) { return { restrict: 'ECA', terminal: true, @@ -155,7 +166,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' animate.leave(element.contents(), element); }; - scope.$watch(srcExp, function ngIncludeWatchAction(src) { + scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { var thisChangeId = ++changeCounter; if (src) { diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index 8e94fe24cc96..ade5ce69bbc0 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -54,7 +54,7 @@ function $InterpolateProvider() { }; - this.$get = ['$parse', '$exceptionHandler', function($parse, $exceptionHandler) { + this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, endSymbolLength = endSymbol.length; @@ -64,6 +64,7 @@ function $InterpolateProvider() { * @function * * @requires $parse + * @requires $sce * * @description * @@ -84,12 +85,10 @@ function $InterpolateProvider() { * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have * embedded expression in order to return an interpolation function. Strings with no * embedded expression will return null for the interpolation function. - * @param {boolean=} isTrustedContext when true, requires that the interpolation string does not - * contain any concatenations - i.e. the interpolation string is a single expression. - * Interpolations for *[src] and *[ng-src] (except IMG, since itwhich sanitizes its value) - * pass true for this parameter. This helps avoid hunting through the template code to - * figure out of some iframe[src], object[src], etc. was interpolated with a concatenation - * that ended up introducing a XSS. + * @param {string=} trustedContext when provided, the returned function passes the interpolated + * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, + * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that + * provides Strict Contextual Escaping for details. * @returns {function(context)} an interpolation function which is used to compute the interpolated * string. The function has these parameters: * @@ -97,7 +96,7 @@ function $InterpolateProvider() { * against. * */ - function $interpolate(text, mustHaveExpression, isTrustedContext) { + function $interpolate(text, mustHaveExpression, trustedContext) { var startIndex, endIndex, index = 0, @@ -135,10 +134,11 @@ function $InterpolateProvider() { // 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 (isTrustedContext && parts.length > 1) { + if (trustedContext && parts.length > 1) { throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nYou may not use multiple expressions when " + - "interpolating this expression.", text); + "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + + "interpolations that concatenate multiple expressions when a trusted value is " + + "required. See http://docs.angularjs.org/api/ng.$sce", text); } if (!mustHaveExpression || hasInterpolation) { @@ -148,6 +148,11 @@ function $InterpolateProvider() { for(var i = 0, ii = length, part; i to learn more about them. + * You can ensure your document is in standards mode and not quirks mode by adding `` + * to the top of your HTML document. + * + * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for + * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * Here's an example of a binding in a privileged context: + * + *
+ *     
+ *     
+ *
+ * + * Notice that `ng-bind-html-unsafe` is bound to `{{userHtml}}` controlled by the user. With SCE + * disabled, this application allows the user to render arbitrary HTML into the DIV. + * In a more realistic example, one may be rendering user comments, blog articles, etc. via + * bindings. (HTML is just one example of a context where rendering user controlled input creates + * security vulnerabilities.) + * + * For the case of HTML, you might use a library, either on the client side, or on the server side, + * to sanitize unsafe HTML before binding to the value and rendering it in the document. + * + * How would you ensure that every place that used these types of bindings was bound to a value that + * was sanitized by your library (or returned as safe for rendering by your server?) How can you + * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some + * properties/fields and forgot to update the binding to the sanitized value? + * + * To be secure by default, you want to ensure that any such bindings are disallowed unless you can + * determine that something explicitly says it's safe to use a value for binding in that + * context. You can then audit your code (a simple grep would do) to ensure that this is only done + * for those values that you can easily tell are safe - because they were received from your server, + * sanitized by your library, etc. You can organize your codebase to help with this - perhaps + * allowing only the files in a specific directory to do this. Ensuring that the internal API + * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * + * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} (and shorthand + * methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to obtain values that will be + * accepted by SCE / privileged contexts. + * + * + * ## How does it work? + * + * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted + * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link + * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. + * + * As an example, {@link ng.directive:ngBindHtmlUnsafe ngBindHtmlUnsafe} uses {@link + * ng.$sce#parseHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly + * simplified): + * + *
+ *   var ngBindHtmlUnsafeDirective = ['$sce', function($sce) {
+ *     return function(scope, element, attr) {
+ *       scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) {
+ *         element.html(value || '');
+ *       });
+ *     };
+ *   }];
+ * 
+ * + * ## Impact on loading templates + * + * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as + * `templateUrl`'s specified by {@link guide/directive directives}. + * + * By default, Angular only loads templates from the same domain and protocol as the application + * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or + * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * + * *Please note*: + * The browser's + * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest + * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing (CORS)} + * policy apply in addition to this and may further restrict whether the template is successfully + * loaded. This means that without the right CORS policy, loading templates from a different domain + * won't work on all browsers. Also, loading templates from `file://` URL does not work on some + * browsers. + * + * ## This feels like too much overhead for the developer? + * + * It's important to remember that SCE only applies to interpolation expressions. + * + * 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. + * + * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them + * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * + * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load + * templates in `ng-include` from your application's domain without having to even know about SCE. + * It blocks loading templates from other domains or loading templates over http from an https + * served document. You can change these by setting your own custom {@link + * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link + * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * + * This significantly reduces the overhead. It is far easier to pay the small overhead and have an + * application that's secure and can be audited to verify that with much more ease than bolting + * security onto an application later. + * + * ## What trusted context types are supported? + * + * | Context | Notes | + * |=====================|================| + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtmlUnsafe ngBindHtmlUnsafe} directive uses this context for bindings. | + * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. 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 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. | + * + * ## Show me an example. + * + * + * + * @example + + +
+ +
Error: {{myCtrl.errorMsg}}
+
+
+ {{userComment.name}}: + +
+
+
+
+ + + // These types of functions would be in the data access layer of your application code. + function fetchUserCommentsFromServer($http, $q, $templateCache, $sce) { + var deferred = $q.defer(); + $http({method: "GET", url: "test_data.json", cache: $templateCache}). + success(function(userComments, status) { + // The comments coming from the server have been sanitized by the server and can be + // trusted. + angular.forEach(userComments, function(userComment) { + userComment.htmlComment = $sce.trustAsHtml(userComment.htmlComment); + }); + deferred.resolve(userComments); + }). + error(function (data, status) { + deferred.reject("HTTP status code " + status + ": " + data); + }); + return deferred.promise; + }; + + var mySceApp = angular.module('mySceApp', []); + + mySceApp.controller("myAppController", function myAppController($injector) { + var self = this; + + self.someHtml = "This might have been any binding including an input element " + + "controlled by the user."; + + self.fetchUserComments = function() { + $injector.invoke(fetchUserCommentsFromServer).then( + function onSuccess(userComments) { + self.errorMsg = null; + self.userComments = userComments; + }, + function onFailure(errorMsg) { + self.errorMsg = errorMsg; + }); + } + }); + + + + [ + { "name": "Alice", + "htmlComment": "Is anyone reading this?" + }, + { "name": "Bob", + "htmlComment": "Yes! Am I the only other one?" + } + ] + + + + describe('SCE doc demo', function() { + it('should bind trusted values', function() { + element('#fetchBtn').click(); + expect(element('.htmlComment').html()).toBe('Is anyone reading this?'); + }); + it('should NOT bind arbitrary values', function() { + expect(element('#someHtml').html()).toBe(''); + }); + }); + +
+ * + * + * + * ## Can I disable SCE completely? + * + * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits + * for little coding overhead. It will be much harder to take an SCE disabled application and + * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE + * for cases where you have a lot of existing code that was written before SCE was introduced and + * you're migrating them a module at a time. + * + * That said, here's how you can completely disable SCE: + * + *
+ *   angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
+ *     // Completely disable SCE.  For demonstration purposes only!
+ *     // Do not use in new projects.
+ *     $sceProvider.enabled(false);
+ *   });
+ * 
+ * + */ + +function $SceProvider() { + var enabled = true; + + /** + * @ngdoc function + * @name ng.sceProvider#enabled + * @methodOf ng.$sceProvider + * @function + * + * @param {boolean=} value If provided, then enables/disables SCE. + * @return {boolean} true if SCE is enabled, false otherwise. + * + * @description + * Enables/disables SCE and returns the current value. + */ + this.enabled = function (value) { + if (arguments.length) { + enabled = !!value; + } + return enabled; + }; + + + /* Design notes on the default implementation for SCE. + * + * The API contract for the SCE delegate + * ------------------------------------- + * The SCE delegate object must provide the following 3 methods: + * + * - trustAs(contextEnum, value) + * This method is used to tell the SCE service that the provided value is OK to use in the + * contexts specified by contextEnum. It must return an object that will be accepted by + * getTrusted() for a compatible contextEnum and return this value. + * + * - valueOf(value) + * For values that were not produced by trustAs(), return them as is. For values that were + * produced by trustAs(), return the corresponding input value to trustAs. Basically, if + * trustAs is wrapping the given values into some type, this operation unwraps it when given + * such a value. + * + * - getTrusted(contextEnum, value) + * This function should return the a 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 opaque + * or wrapped in some holder object. That happens to be an implementation detail. For instance, + * an implementation could maintain a registry of all trusted objects by context. In such a case, + * trustAs() would return the same object that was passed in. getTrusted() would return the same + * object passed in if it was found in the registry under a compatible context or throw an + * exception otherwise. An implementation might only wrap values some of the time based on + * some criteria. getTrusted() might return a value and not throw an exception for special + * constants or objects even if not wrapped. All such implementations fulfill this contract. + * + * + * A note on the inheritance model for SCE contexts + * ------------------------------------------------ + * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This + * is purely an implementation details. + * + * The contract is simply this: + * + * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) + * will also succeed. + * + * Inheritance happens to capture this in a natural way. In some future, we + * may not use inheritance anymore. That is OK because no code outside of + * sce.js and sceSpecs.js would need to be aware of this detail. + */ + + this.$get = ['$parse', '$document', '$sceDelegate', function( + $parse, $document, $sceDelegate) { + // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + // the "expression(javascript expression)" syntax which is insecure. + if (enabled && msie) { + var documentMode = $document[0].documentMode; + if (documentMode !== undefined && documentMode < 8) { + throw $sceMinErr('iequirks', + 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + 'mode. You can fix this by adding the text to the top of your HTML ' + + 'document. See http://docs.angularjs.org/api/ng.$sce for more information.'); + } + } + + var sce = copy(SCE_CONTEXTS); + + /** + * @ngdoc function + * @name ng.sce#isEnabled + * @methodOf ng.$sce + * @function + * + * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you + * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. + * + * @description + * Returns a boolean indicating if SCE is enabled. + */ + sce.isEnabled = function () { + return enabled; + }; + sce.trustAs = $sceDelegate.trustAs; + sce.getTrusted = $sceDelegate.getTrusted; + sce.valueOf = $sceDelegate.valueOf; + + if (!enabled) { + sce.trustAs = sce.getTrusted = function(type, value) { return value; }, + sce.valueOf = identity + } + + /** + * @ngdoc method + * @name ng.$sce#parse + * @methodOf ng.$sce + * + * @description + * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it + * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, + * *result*)} + * + * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + sce.parseAs = function sceParseAs(type, expr) { + var parsed = $parse(expr); + if (parsed.literal && parsed.constant) { + return parsed; + } else { + return function sceParseAsTrusted(self, locals) { + return sce.getTrusted(type, parsed(self, locals)); + } + } + }; + + /** + * @ngdoc method + * @name ng.$sce#trustAs + * @methodOf ng.$sce + * + * @description + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns an object + * that is trusted by angular for use in specified strict contextual escaping contexts (such as + * ng-html-bind-unsafe, ng-include, any src attribute interpolation, any dom event binding + * attribute interpolation such as for onclick, etc.) that uses the provided value. See * + * {@link ng.$sce $sce} for enabling strict contextual escaping. + * + * @param {string} type The kind of context in which this value is safe for use. e.g. url, + * resource_url, html, js and css. + * @param {*} value The value that that should be considered trusted/safe. + * @returns {*} A value that can be used to stand in for the provided `value` in places + * where Angular expects a $sce.trustAs() return value. + */ + + /** + * @ngdoc method + * @name ng.$sce#trustAsHtml + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.trustAsHtml(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml + * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name ng.$sce#trustAsUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.trustAsUrl(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl + * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name ng.$sce#trustAsResourceUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.trustAsResourceUrl(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl + * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the return + * value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name ng.$sce#trustAsJs + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.trustAsJs(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} + * + * @param {*} value The value to trustAs. + * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs + * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives + * only accept expressions that are either literal constants or are the + * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrusted + * @methodOf ng.$sce + * + * @description + * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, takes + * the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the originally supplied + * value if the queried context type is a supertype of the created type. If this condition + * isn't satisfied, throws an exception. + * + * @param {string} type The kind of context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} call. + * @returns {*} The value the was originally provided to {@link ng.$sce#trustAs `$sce.trustAs`} if + * valid in this context. Otherwise, throws an exception. + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrustedHtml + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.getTrustedHtml(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrustedCss + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.getTrustedCss(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrustedUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.getTrustedUrl(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrustedResourceUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.getTrustedResourceUrl(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} + * + * @param {*} value The value to pass to `$sceDelegate.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + */ + + /** + * @ngdoc method + * @name ng.$sce#getTrustedJs + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.getTrustedJs(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} + * + * @param {*} value The value to pass to `$sce.getTrusted`. + * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + */ + + /** + * @ngdoc method + * @name ng.$sce#parseAsHtml + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.parseAsHtml(expression string)` → {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name ng.$sce#parseAsCss + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.parseAsCss(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name ng.$sce#parseAsUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.parseAsUrl(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name ng.$sce#parseAsResourceUrl + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.parseAsResourceUrl(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + /** + * @ngdoc method + * @name ng.$sce#parseAsJs + * @methodOf ng.$sce + * + * @description + * Shorthand method. `$sce.parseAsJs(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * + * @param {string} expression String expression to compile. + * @returns {function(context, locals)} a function which represents the compiled expression: + * + * * `context` – `{object}` – an object against which any expressions embedded in the strings + * are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values in + * `context`. + */ + + // Shorthand delegations. + var parse = sce.parseAs, + getTrusted = sce.getTrusted, + trustAs = sce.trustAs; + + angular.forEach(SCE_CONTEXTS, function (enumValue, name) { + var lName = lowercase(name); + sce[camelCase("parse_as_" + lName)] = function (expr) { + return parse(enumValue, expr); + } + sce[camelCase("get_trusted_" + lName)] = function (value) { + return getTrusted(enumValue, value); + } + sce[camelCase("trust_as_" + lName)] = function (value) { + return trustAs(enumValue, value); + } + }); + + return sce; + }]; +} diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js index 5402b5004c4f..af2d913fc583 100644 --- a/src/ng/urlUtils.js +++ b/src/ng/urlUtils.js @@ -105,11 +105,12 @@ function $$UrlUtilsProvider() { /** * Parse a request URL and determine whether this is a same-origin request as the application document. * - * @param {string} requestUrl The url of the request. + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. * @returns {boolean} Whether the request is for the same origin as the application document. */ isSameOrigin: function isSameOrigin(requestUrl) { - var parsed = resolve(requestUrl, true); + var parsed = (typeof requestUrl === 'string') ? resolve(requestUrl, true) : requestUrl; return (parsed.protocol === originUrl.protocol && parsed.host === originUrl.host); } diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index de0d90160353..743897d625c5 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -56,9 +56,9 @@ function $RouteProvider(){ * if passed as a string. * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be * published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or function that returns - * an html template as a string which should be used by {@link ngRoute.directive:ngView ngView} or - * {@link ng.directive:ngInclude ngInclude} directives. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used by {@link + * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. * This property takes precedence over `templateUrl`. * * If `template` is a function, it will be called with the following parameters: @@ -149,8 +149,8 @@ function $RouteProvider(){ }; - this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', - function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache) { + this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', '$sce', + function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { /** * @ngdoc object @@ -437,7 +437,7 @@ function $RouteProvider(){ then(function() { if (next) { var locals = extend({}, next.resolve), - template; + template, templateUrl; forEach(locals, function(value, key) { locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); @@ -447,13 +447,14 @@ function $RouteProvider(){ if (isFunction(template)) { template = template(next.params); } - } else if (isDefined(template = next.templateUrl)) { - if (isFunction(template)) { - template = template(next.params); + } else if (isDefined(templateUrl = next.templateUrl)) { + if (isFunction(templateUrl)) { + templateUrl = templateUrl(next.params); } - if (isDefined(template)) { - next.loadedTemplateUrl = template; - template = $http.get(template, {cache: $templateCache}). + templateUrl = $sce.getTrustedResourceUrl(templateUrl); + if (isDefined(templateUrl)) { + next.loadedTemplateUrl = templateUrl; + template = $http.get(templateUrl, {cache: $templateCache}). then(function(response) { return response.data; }); } } diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index 049a68211332..110b3a64f682 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -63,11 +63,15 @@ var ngSanitizeMinErr = angular.$$minErr('ngSanitize');
@@ -94,8 +98,8 @@ var ngSanitizeMinErr = angular.$$minErr('ngSanitize'); unsafe html filter -
<div ng-bind-html-unsafe="snippet">
</div>
-
+
<div ng-bind-html-unsafe="getSceSnippet()">
</div>
+
@@ -120,11 +124,11 @@ var ngSanitizeMinErr = angular.$$minErr('ngSanitize'); "snippet

"); }); - it('should update', function() { + it('should update', function($sce) { input('snippet').enter('new text'); expect(using('#html-filter').binding('snippet')).toBe('new text'); expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); - expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); + expect(using('#html-unsafe-filter').element('div').html()).toBe('new text'); });
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index c782187878d6..1f5aae95943f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -681,9 +681,17 @@ describe('$compile', function() { restrict: 'CAM', templateUrl: 'hello.html', transclude: true })); directive('cau', valueFn({ - restrict: 'CAM', templateUrl:'cau.html' + restrict: 'CAM', templateUrl: 'cau.html' })); - + directive('crossDomainTemplate', valueFn({ + restrict: 'CAM', templateUrl: 'http://example.com/should-not-load.html' + })); + directive('trustedTemplate', function($sce) { return { + restrict: 'CAM', + templateUrl: function() { + return $sce.trustAsResourceUrl('http://example.com/trusted-template.html'); + }}; + }); directive('cError', valueFn({ restrict: 'CAM', templateUrl:'error.html', @@ -735,6 +743,24 @@ describe('$compile', function() { } )); + it('should not load cross domain templates by default', inject( + function($compile, $rootScope, $templateCache, $sce) { + expect(function() { + $templateCache.put('http://example.com/should-not-load.html', 'Should not load even if in cache.'); + $compile('
')($rootScope); + }).toThrow('[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: http://example.com/should-not-load.html'); + })); + + it('should load cross domain templates when trusted', inject( + function($compile, $httpBackend, $rootScope, $sce) { + $httpBackend.expect('GET', 'http://example.com/trusted-template.html').respond('example.com/trusted_template_contents'); + element = $compile('
')($rootScope); + expect(sortedHtml(element)). + toEqual('
'); + $httpBackend.flush(); + expect(sortedHtml(element)). + toEqual('
example.com/trusted_template_contents
'); + })); it('should append template via $http and cache it in $templateCache', inject( function($compile, $httpBackend, $templateCache, $rootScope, $browser) { @@ -1521,6 +1547,16 @@ describe('$compile', function() { expect(element.attr('name')).toEqual('attr: angular'); })); + describe('SCE values', function() { + it('should resolve compile and link both attribute and text bindings', inject( + function($rootScope, $compile, $sce) { + $rootScope.name = $sce.trustAsHtml('angular'); + element = $compile('
text: {{name}}
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('text: angular'); + expect(element.attr('name')).toEqual('attr: angular'); + })); + }); it('should decorate the binding with ng-binding and interpolation function', inject( function($compile, $rootScope) { @@ -2625,12 +2661,16 @@ describe('$compile', function() { }); - describe('img[src] sanitization', function() { - it('should NOT require trusted values for img src', inject(function($rootScope, $compile) { + describe('img[src] sanitization', function($sce) { + 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'); })); it('should sanitize javascript: urls', inject(function($compile, $rootScope) { @@ -2965,6 +3005,48 @@ describe('$compile', function() { })); }); + describe('iframe[src]', function() { + it('should pass through src attributes for the same domain', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = "different_page"; + $rootScope.$apply(); + expect(element.attr('src')).toEqual('different_page'); + })); + + it('should clear out src attributes for a different domain', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = "http://a.different.domain.example.com"; + expect(function() { $rootScope.$apply() }).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: " + + "http://a.different.domain.example.com"); + })); + + it('should clear out JS src attributes', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = "javascript:alert(1);"; + expect(function() { $rootScope.$apply() }).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: " + + "javascript:alert(1);"); + })); + + it('should clear out non-resource_url src attributes', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl("javascript:doTrustedStuff()"); + expect($rootScope.$apply).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: javascript:doTrustedStuff()"); + })); + + it('should pass through $sce.trustAs() values in src attributes', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsResourceUrl("javascript:doTrustedStuff()"); + $rootScope.$apply(); + + expect(element.attr('src')).toEqual('javascript:doTrustedStuff()'); + })); + }); describe('ngAttr* attribute binding', function() { diff --git a/test/ng/directive/booleanAttrsSpec.js b/test/ng/directive/booleanAttrsSpec.js index be2dfb6041e5..93e8cc20f7f2 100644 --- a/test/ng/directive/booleanAttrsSpec.js +++ b/test/ng/directive/booleanAttrsSpec.js @@ -102,61 +102,99 @@ describe('boolean attr directives', function() { describe('ngSrc', function() { - it('should interpolate the expression and bind to src', inject(function($compile, $rootScope) { + it('should interpolate the expression and bind to src with raw same-domain value', + inject(function($compile, $rootScope) { + var element = $compile('
')($rootScope); + + $rootScope.$digest(); + expect(element.attr('src')).toBeUndefined(); + + $rootScope.$apply(function() { + $rootScope.id = '/somewhere/here'; + }); + expect(element.attr('src')).toEqual('/somewhere/here'); + + dealoc(element); + })); + + + it('should interpolate the expression and bind to src with a trusted value', inject(function($compile, $rootScope, $sce) { var element = $compile('
')($rootScope); $rootScope.$digest(); expect(element.attr('src')).toBeUndefined(); $rootScope.$apply(function() { - $rootScope.id = 1; + $rootScope.id = $sce.trustAsResourceUrl('http://somewhere'); }); - expect(element.attr('src')).toEqual('1'); + expect(element.attr('src')).toEqual('http://somewhere'); dealoc(element); })); - describe('isTrustedContext', function() { - it('should NOT interpolate a multi-part expression for non-img src attribute', inject(function($compile, $rootScope) { - expect(function() { - var element = $compile('
')($rootScope); - dealoc(element); - }).toThrow( - "[$interpolate:noconcat] Error while interpolating: some/{{id}}\nYou may not use " + - "multiple expressions when interpolating this expression."); - })); - it('should interpolate a multi-part expression for regular attributes', inject(function($compile, $rootScope) { - var element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(element.attr('foo')).toBe('some/'); + it('should NOT interpolate a multi-part expression for non-img src attribute', inject(function($compile, $rootScope) { + expect(function() { + var element = $compile('
')($rootScope); + dealoc(element); + }).toThrow( + "[$interpolate:noconcat] Error while interpolating: some/{{id}}\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 for regular attributes', inject(function($compile, $rootScope) { + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.attr('foo')).toBe('some/'); + $rootScope.$apply(function() { + $rootScope.id = 1; + }); + expect(element.attr('foo')).toEqual('some/1'); + })); + + + it('should NOT interpolate a wrongly typed expression', inject(function($compile, $rootScope, $sce) { + expect(function() { + var element = $compile('
')($rootScope); $rootScope.$apply(function() { - $rootScope.id = 1; + $rootScope.id = $sce.trustAsUrl('http://somewhere'); }); - expect(element.attr('foo')).toEqual('some/1'); - })); + element.attr('src'); + }).toThrow( + "[$interpolate:interr] Can't interpolate: {{id}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: http://somewhere"); + })); - }); if (msie) { it('should update the element property as well as the attribute', inject( - function($compile, $rootScope) { - // 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 + function($compile, $rootScope, $sce) { + // 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 - var element = $compile('
')($rootScope); + var element = $compile('
')($rootScope); - $rootScope.$digest(); - expect(element.prop('src')).toBeUndefined(); + $rootScope.$digest(); + expect(element.prop('src')).toBeUndefined(); + dealoc(element); - $rootScope.$apply(function() { - $rootScope.id = 1; - }); - expect(element.prop('src')).toEqual('1'); + element = $compile('
')($rootScope); - dealoc(element); - })); + $rootScope.$digest(); + expect(element.prop('src')).toEqual('some/'); + dealoc(element); + + element = $compile('
')($rootScope); + $rootScope.$apply(function() { + $rootScope.id = $sce.trustAsResourceUrl('http://somewhere'); + }); + expect(element.prop('src')).toEqual('http://somewhere'); + + dealoc(element); + })); } }); diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index da291fa43ceb..1d8f8ef4d906 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -69,11 +69,47 @@ describe('ngBind*', function() { describe('ngBindHtmlUnsafe', function() { - it('should set unsafe html', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.html = '
hello
'; - $rootScope.$digest(); - expect(angular.lowercase(element.html())).toEqual('
hello
'); - })); + function configureSce(enabled) { + module(function($provide, $sceProvider) { + $sceProvider.enabled(enabled); + }); + }; + + describe('SCE disabled', function() { + beforeEach(function() {configureSce(false)}); + + it('should set unsafe html', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.html = '
hello
'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('
hello
'); + })); + }); + + + describe('SCE enabled', function() { + beforeEach(function() {configureSce(true)}); + + it('should NOT set unsafe html for untrusted values', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.html = '
hello
'; + expect($rootScope.$digest).toThrow(); + })); + + it('should NOT set unsafe html for wrongly typed values', inject(function($rootScope, $compile, $sce) { + element = $compile('
')($rootScope); + $rootScope.html = $sce.trustAsCss('
hello
'); + expect($rootScope.$digest).toThrow(); + })); + + it('should set unsafe html for trusted values', inject(function($rootScope, $compile, $sce) { + element = $compile('
')($rootScope); + $rootScope.html = $sce.trustAsHtml('
hello
'); + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('
hello
'); + })); + + }); + }); }); diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index 937094310bef..6cb7875502b1 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -3,7 +3,6 @@ describe('ngInclude', function() { var element; - afterEach(function(){ dealoc(element); }); @@ -16,7 +15,29 @@ describe('ngInclude', function() { } - it('should include on external file', inject(putIntoCache('myUrl', '{{name}}'), + it('should trust and use literal urls', inject(function( + $rootScope, $httpBackend, $compile) { + element = $compile('
')($rootScope); + $httpBackend.expect('GET', 'url').respond('template text'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('template text'); + dealoc($rootScope); + })); + + + it('should trust and use trusted urls', inject(function($rootScope, $httpBackend, $compile, $sce) { + element = $compile('
')($rootScope); + $httpBackend.expect('GET', 'http://foo.bar/url').respond('template text'); + $rootScope.fooUrl = $sce.trustAsResourceUrl('http://foo.bar/url'); + $rootScope.$digest(); + $httpBackend.flush(); + expect(element.text()).toEqual('template text'); + dealoc($rootScope); + })); + + + it('should include an external file', inject(putIntoCache('myUrl', '{{name}}'), function($rootScope, $compile) { element = jqLite(''); jqLite(document.body).append(element); @@ -42,6 +63,29 @@ describe('ngInclude', function() { })); + it('should NOT use untrusted expressions ', inject(putIntoCache('myUrl', '{{name}} text'), + function($rootScope, $compile, $sce) { + element = jqLite(''); + jqLite(document.body).append(element); + element = $compile(element)($rootScope); + $rootScope.name = 'chirayu'; + $rootScope.url = 'myUrl'; + expect($rootScope.$digest).toThrow(); + jqLite(document.body).html(''); + })); + + + it('should NOT use mistyped expressions ', inject(putIntoCache('myUrl', '{{name}} text'), + function($rootScope, $compile, $sce) { + element = jqLite(''); + jqLite(document.body).append(element); + element = $compile(element)($rootScope); + $rootScope.name = 'chirayu'; + $rootScope.url = $sce.trustAsUrl('myUrl'); + expect($rootScope.$digest).toThrow(); + jqLite(document.body).html(''); + })); + it('should remove previously included text if a falsy value is bound to src', inject( putIntoCache('myUrl', '{{name}}'), function($rootScope, $compile) { @@ -308,7 +352,7 @@ describe('ngInclude ngAnimate', function() { } function applyCSS(element, cssProp, cssValue) { - element.css(cssProp, cssValue); + element.css(cssProp, cssValue); element.css(vendorPrefix + cssProp, cssValue); } diff --git a/test/ng/directive/ngSrcSpec.js b/test/ng/directive/ngSrcSpec.js index a917c5116bec..23ace7ee7c2b 100644 --- a/test/ng/directive/ngSrcSpec.js +++ b/test/ng/directive/ngSrcSpec.js @@ -14,4 +14,48 @@ describe('ngSrc', function() { expect(element.attr('src')).not.toBe(''); expect(element.attr('src')).toBe(undefined); })); + + describe('iframe[ng-src]', function() { + it('should pass through src attributes for the same domain', inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + $rootScope.testUrl = "different_page"; + $rootScope.$apply(); + expect(element.attr('src')).toEqual('different_page'); + })); + + it('should error on src attributes for a different domain', inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + $rootScope.testUrl = "http://a.different.domain.example.com"; + expect(function() { $rootScope.$apply() }).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: " + + "http://a.different.domain.example.com"); + })); + + it('should error on JS src attributes', inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + $rootScope.testUrl = "javascript:alert(1);"; + expect(function() { $rootScope.$apply() }).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: " + + "javascript:alert(1);"); + })); + + it('should error on non-resource_url src attributes', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsUrl("javascript:doTrustedStuff()"); + expect($rootScope.$apply).toThrow( + "[$interpolate:interr] Can't interpolate: {{testUrl}}\nError: [$sce:isecrurl] Blocked " + + "loading resource from url not allowed by $sceDelegate policy. URL: " + + "javascript:doTrustedStuff()"); + })); + + it('should pass through $sce.trustAs() values in src attributes', inject(function($compile, $rootScope, $sce) { + element = $compile('')($rootScope); + $rootScope.testUrl = $sce.trustAsResourceUrl("javascript:doTrustedStuff()"); + $rootScope.$apply(); + + expect(element.attr('src')).toEqual('javascript:doTrustedStuff()'); + })); + }); }); diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 7569c0e2e96b..d74b764a5078 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -67,6 +67,55 @@ describe('$interpolate', function() { })); + describe('interpolating in a trusted context', function() { + var sce; + beforeEach(function() { + function log() {}; + var fakeLog = {log: log, warn: log, info: log, error: log}; + module(function($provide, $sceProvider) { + $provide.value('$log', fakeLog); + $sceProvider.enabled(true); + }); + inject(['$sce', function($sce) { sce = $sce; }]); + }); + + it('should NOT interpolate non-trusted expressions', inject(function($interpolate) { + var foo = "foo"; + expect($interpolate('{{foo}}', true, sce.CSS)({}, {foo: foo})).toEqual(''); + })); + + it('should NOT interpolate mistyped expressions', inject(function($interpolate) { + var foo = sce.trustAsCss("foo"); + expect($interpolate('{{foo}}', true, sce.HTML)({}, {foo: foo})).toEqual(''); + })); + + it('should interpolate trusted expressions in a regular context', inject(function($interpolate) { + var foo = sce.trustAsCss("foo"); + expect($interpolate('{{foo}}', true)({foo: foo})).toEqual('foo'); + })); + + it('should interpolate trusted expressions in a specific trustedContext', inject(function($interpolate) { + var foo = sce.trustAsCss("foo"); + expect($interpolate('{{foo}}', true, sce.CSS)({foo: foo})).toEqual('foo'); + })); + + // 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.) + 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}); }).toThrow( + "[$interpolate:noconcat] 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"); + })); + }); + + describe('provider', function() { beforeEach(module(function($interpolateProvider) { $interpolateProvider.startSymbol('--'); @@ -155,13 +204,15 @@ describe('$interpolate', function() { expect(function() { $interpolate('constant/{{var}}', true, isTrustedContext); }).toThrow( - "[$interpolate:noconcat] Error while interpolating: constant/{{var}}\nYou may not use " + - "multiple expressions when interpolating this expression."); + "[$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('{{foo}}{{bar}}', true, isTrustedContext); }).toThrow( - "[$interpolate:noconcat] Error while interpolating: {{foo}}{{bar}}\nYou may not use " + - "multiple expressions when interpolating this expression."); + "[$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) { diff --git a/test/ng/sceSpecs.js b/test/ng/sceSpecs.js new file mode 100644 index 000000000000..16525b8d02f0 --- /dev/null +++ b/test/ng/sceSpecs.js @@ -0,0 +1,347 @@ +'use strict'; + +describe('SCE', function() { + + describe('when disabled', function() { + beforeEach(function() { + module(function($sceProvider) { + $sceProvider.enabled(false); + }); + }); + + it('should provide the getter for enabled', inject(function($sce) { + expect($sce.isEnabled()).toBe(false); + })); + + it('should not wrap/unwrap any value or throw exception on non-string values', inject(function($sce) { + var originalValue = { foo: "bar" }; + expect($sce.trustAs($sce.JS, originalValue)).toBe(originalValue); + expect($sce.getTrusted($sce.JS, originalValue)).toBe(originalValue); + })); + }); + + describe('IE8 quirks mode', function() { + function runTest(enabled, documentMode, expectException) { + module(function($provide) { + $provide.value('$document', [{ + documentMode: documentMode, + createElement: function() {} + }]); + $provide.value('$sceDelegate', {trustAs: null, valueOf: null, getTrusted: null}); + }); + + inject(function($window, $injector) { + function constructSce() { + var sceProvider = new $SceProvider(); + sceProvider.enabled(enabled); + return $injector.invoke(sceProvider.$get, sceProvider); + } + + var origMsie = $window.msie; + try { + $window.msie = true; + if (expectException) { + expect(constructSce).toThrow( + '[$sce:iequirks] Strict Contextual Escaping does not support Internet Explorer ' + + 'version < 9 in quirks mode. You can fix this by adding the text to ' + + 'the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more ' + + 'information.'); + } else { + // no exception. + constructSce(); + } + } + finally { + $window.msie = origMsie; + } + }); + } + + it('should throw an exception when sce is enabled in quirks mode', function() { + runTest(true, 7, true); + }); + + it('should NOT throw an exception when sce is enabled and in standards mode', function() { + runTest(true, 8, false); + }); + + it('should NOT throw an exception when sce is enabled and documentMode is undefined', function() { + runTest(true, undefined, false); + }); + + it('should NOT throw an exception when sce is disabled even when in quirks mode', function() { + runTest(false, 7, false); + }); + + it('should NOT throw an exception when sce is disabled and in standards mode', function() { + runTest(false, 8, false); + }); + + it('should NOT throw an exception when sce is disabled and documentMode is undefined', function() { + runTest(false, undefined, false); + }); + }); + + describe('when enabled', function() { + it('should wrap string values with TrustedValueHolder', inject(function($sce) { + var originalValue = 'original_value'; + var wrappedValue = $sce.trustAs($sce.HTML, originalValue); + expect(typeof wrappedValue).toBe('object'); + expect($sce.getTrusted($sce.HTML, wrappedValue)).toBe('original_value'); + expect(function() { $sce.getTrusted($sce.CSS, wrappedValue); }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + wrappedValue = $sce.trustAs($sce.CSS, originalValue); + expect(typeof wrappedValue).toBe('object'); + expect($sce.getTrusted($sce.CSS, wrappedValue)).toBe('original_value'); + expect(function() { $sce.getTrusted($sce.HTML, wrappedValue); }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + wrappedValue = $sce.trustAs($sce.URL, originalValue); + expect(typeof wrappedValue).toBe('object'); + expect($sce.getTrusted($sce.URL, wrappedValue)).toBe('original_value'); + wrappedValue = $sce.trustAs($sce.JS, originalValue); + expect(typeof wrappedValue).toBe('object'); + expect($sce.getTrusted($sce.JS, wrappedValue)).toBe('original_value'); + })); + + it('should NOT wrap non-string values', inject(function($sce) { + expect(function() { $sce.trustAsCss(123); }).toThrow( + '[$sce:itype] Attempted to trust a non-string value in a content requiring a string: ' + + 'Context: css'); + })); + + it('should NOT wrap unknown contexts', inject(function($sce) { + expect(function() { $sce.trustAs('unknown1' , '123'); }).toThrow( + '[$sce:icontext] Attempted to trust a value in invalid context. Context: unknown1; Value: 123'); + })); + + it('should NOT wrap undefined context', inject(function($sce) { + expect(function() { $sce.trustAs(undefined, '123'); }).toThrow( + '[$sce:icontext] Attempted to trust a value in invalid context. Context: undefined; Value: 123'); + })); + + it('should wrap undefined into undefined', inject(function($sce) { + expect($sce.trustAsHtml(undefined)).toBe(undefined); + })); + + it('should unwrap undefined into undefined', inject(function($sce) { + expect($sce.getTrusted($sce.HTML, undefined)).toBe(undefined); + })); + + it('should wrap null into null', inject(function($sce) { + expect($sce.trustAsHtml(null)).toBe(null); + })); + + it('should unwrap null into null', inject(function($sce) { + expect($sce.getTrusted($sce.HTML, null)).toBe(null); + })); + + it('should wrap "" into ""', inject(function($sce) { + expect($sce.trustAsHtml("")).toBe(""); + })); + + it('should unwrap null into null', inject(function($sce) { + expect($sce.getTrusted($sce.HTML, null)).toBe(null); + })); + + it('should unwrap "" into ""', inject(function($sce) { + expect($sce.getTrusted($sce.HTML, "")).toBe(""); + })); + + it('should unwrap values and return the original', inject(function($sce) { + var originalValue = "originalValue"; + var wrappedValue = $sce.trustAs($sce.HTML, originalValue); + expect($sce.getTrusted($sce.HTML, wrappedValue)).toBe(originalValue); + })); + + it('should NOT unwrap values when the type is different', inject(function($sce) { + var originalValue = "originalValue"; + var wrappedValue = $sce.trustAs($sce.HTML, originalValue); + expect(function () { $sce.getTrusted($sce.CSS, wrappedValue); }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + })); + + it('should NOT unwrap values that had not been wrapped', inject(function($sce) { + function TrustedValueHolder(trustedValue) { + this.$unwrapTrustedValue = function() { + return trustedValue; + }; + } + var wrappedValue = new TrustedValueHolder("originalValue"); + expect(function() { return $sce.getTrusted($sce.HTML, wrappedValue) }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + })); + + it('should implement toString on trusted values', inject(function($sce) { + var originalValue = '123', + wrappedValue = $sce.trustAsHtml(originalValue); + expect($sce.getTrustedHtml(wrappedValue)).toBe(originalValue); + expect(wrappedValue.toString()).toBe(originalValue.toString()); + })); + }); + + + describe('replace $sceDelegate', function() { + it('should override the default $sce.trustAs/valueOf/etc.', function() { + module(function($provide) { + $provide.value('$sceDelegate', { + trustAs: function(type, value) { return "wrapped:" + value; }, + getTrusted: function(type, value) { return "unwrapped:" + value; }, + valueOf: function(value) { return "valueOf:" + value; } + }); + }); + + inject(function($sce) { + expect($sce.trustAsJs("value")).toBe("wrapped:value"); + expect($sce.valueOf("value")).toBe("valueOf:value"); + expect($sce.getTrustedJs("value")).toBe("unwrapped:value"); + expect($sce.parseAsJs("name")({name: "chirayu"})).toBe("unwrapped:chirayu"); + }); + }); + }); + + + describe('$sce.parseAs', function($sce) { + it('should parse constant literals as trusted', inject(function($sce) { + expect($sce.parseAsJs('1')()).toBe(1); + expect($sce.parseAsJs('1', $sce.ANY)()).toBe(1); + expect($sce.parseAsJs('1', $sce.HTML)()).toBe(1); + expect($sce.parseAsJs('1', 'UNDEFINED')()).toBe(1); + expect($sce.parseAsJs('true')()).toBe(true); + expect($sce.parseAsJs('false')()).toBe(false); + expect($sce.parseAsJs('null')()).toBe(null); + expect($sce.parseAsJs('undefined')()).toBe(undefined); + expect($sce.parseAsJs('"string"')()).toBe("string"); + })); + + 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. + var exprFn = $sce.parseAsJs('1+1'); + expect(exprFn).toThrow(); + })); + + it('should NOT return untrusted values from expression function', inject(function($sce) { + var exprFn = $sce.parseAs($sce.HTML, 'foo'); + expect(function() { + return exprFn({}, {'foo': true}) + }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + })); + + it('should NOT return trusted values of the wrong type from expression function', inject(function($sce) { + var exprFn = $sce.parseAs($sce.HTML, 'foo'); + expect(function() { + return exprFn({}, {'foo': $sce.trustAs($sce.JS, '123')}) + }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + })); + + it('should return trusted values from expression function', inject(function($sce) { + var exprFn = $sce.parseAs($sce.HTML, 'foo'); + expect(exprFn({}, {'foo': $sce.trustAs($sce.HTML, 'trustedValue')})).toBe('trustedValue'); + })); + + it('should support shorthand methods', inject(function($sce) { + // Test shorthand parse methods. + expect($sce.parseAsHtml('1')()).toBe(1); + // Test short trustAs methods. + expect($sce.trustAsAny).toBeUndefined(); + expect(function() { + // mismatched types. + $sce.parseAsCss('foo')({}, {'foo': $sce.trustAsHtml('1')}); + }).toThrow( + '[$sce:unsafe] Attempting to use an unsafe value in a safe context.'); + })); + + }); + + describe('$sceDelegate resource url policies', function() { + function runTest(cfg, testFn) { + return function() { + module(function($sceDelegateProvider) { + if (cfg.whiteList !== undefined) { + $sceDelegateProvider.resourceUrlWhitelist(cfg.whiteList); + } + if (cfg.blackList !== undefined) { + $sceDelegateProvider.resourceUrlBlacklist(cfg.blackList); + } + }); + inject(testFn); + } + } + + it('should default to "self" which allows relative urls', runTest({}, function($sce, $document) { + expect($sce.getTrustedResourceUrl('foo/bar')).toEqual('foo/bar'); + })); + + it('should reject everything when whitelist is empty', runTest( + { + whiteList: [], + blackList: [] + }, function($sce) { + expect(function() { $sce.getTrustedResourceUrl('#'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: #'); + })); + + it('should match against normalized urls', runTest( + { + whiteList: [/^foo$/], + blackList: [] + }, function($sce) { + expect(function() { $sce.getTrustedResourceUrl('foo'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: foo'); + })); + + it('should support custom regex', runTest( + { + whiteList: [/^http:\/\/example\.com.*/], + blackList: [] + }, function($sce) { + expect($sce.getTrustedResourceUrl('http://example.com/foo')).toEqual('http://example.com/foo'); + expect(function() { $sce.getTrustedResourceUrl('https://example.com/foo'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: https://example.com/foo'); + })); + + it('should support the special string "self" in whitelist', runTest( + { + whiteList: ['self'], + blackList: [] + }, function($sce) { + expect($sce.getTrustedResourceUrl('foo')).toEqual('foo'); + })); + + it('should support the special string "self" in blacklist', runTest( + { + whiteList: [/.*/], + blackList: ['self'] + }, function($sce) { + expect(function() { $sce.getTrustedResourceUrl('foo'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: foo'); + })); + + it('should have blacklist override the whitelist', runTest( + { + whiteList: ['self'], + blackList: ['self'] + }, function($sce) { + expect(function() { $sce.getTrustedResourceUrl('foo'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: foo'); + })); + + it('should support multiple items in both lists', runTest( + { + whiteList: [/^http:\/\/example.com\/1$/, /^http:\/\/example.com\/2$/, /^http:\/\/example.com\/3$/, 'self'], + blackList: [/^http:\/\/example.com\/3$/, /open_redirect/], + }, function($sce) { + expect($sce.getTrustedResourceUrl('same_domain')).toEqual('same_domain'); + expect($sce.getTrustedResourceUrl('http://example.com/1')).toEqual('http://example.com/1'); + expect($sce.getTrustedResourceUrl('http://example.com/2')).toEqual('http://example.com/2'); + expect(function() { $sce.getTrustedResourceUrl('http://example.com/3'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: http://example.com/3'); + expect(function() { $sce.getTrustedResourceUrl('open_redirect'); }).toThrow( + '[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: open_redirect'); + })); + + }); +}); + diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js index 57043a5af855..3c9bf847602c 100644 --- a/test/ng/urlUtilsSpec.js +++ b/test/ng/urlUtilsSpec.js @@ -11,21 +11,27 @@ describe('$$urlUtils', function() { expect(parsed.href).toMatch(/https?:\/\//); expect(parsed.protocol).toMatch(/^https?:/); expect(parsed.host).not.toBe(""); + expect(parsed.hostname).not.toBe(""); + expect(parsed.pathname).not.toBe(""); })); }); describe('isSameOrigin', function() { - it('should support various combinations of urls', inject(function($$urlUtils, $document) { - expect($$urlUtils.isSameOrigin('path')).toBe(true); + it('should support various combinations of urls - both string and parsed', inject(function($$urlUtils, $document) { + function expectIsSameOrigin(url, expectedValue) { + expect($$urlUtils.isSameOrigin(url)).toBe(expectedValue); + expect($$urlUtils.isSameOrigin($$urlUtils.resolve(url, true))).toBe(expectedValue); + } + expectIsSameOrigin('path', true); var origin = $$urlUtils.resolve($document[0].location.href, true); - expect($$urlUtils.isSameOrigin('//' + origin.host + '/path')).toBe(true); + expectIsSameOrigin('//' + origin.host + '/path', true); // Different domain. - expect($$urlUtils.isSameOrigin('http://example.com/path')).toBe(false); + expectIsSameOrigin('http://example.com/path', false); // Auto fill protocol. - expect($$urlUtils.isSameOrigin('//example.com/path')).toBe(false); + expectIsSameOrigin('//example.com/path', false); // Should not match when the ports are different. // This assumes that the test is *not* running on port 22 (very unlikely). - expect($$urlUtils.isSameOrigin('//' + origin.hostname + ':22/path')).toBe(false); + expectIsSameOrigin('//' + origin.hostname + ':22/path', false); })); }); }); diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 300ca2d790e0..13d149a12795 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -13,6 +13,7 @@ describe('$route', function() { $httpBackend.when('GET', 'foo.html').respond('foo'); $httpBackend.when('GET', 'baz.html').respond('baz'); $httpBackend.when('GET', 'bar.html').respond('bar'); + $httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template'); $httpBackend.when('GET', '404.html').respond('not found'); }; })); @@ -510,6 +511,33 @@ describe('$route', function() { }); }); + it('should NOT load cross domain templates by default', function() { + module(function($routeProvider) { + $routeProvider.when('/foo', { templateUrl: 'http://example.com/foo.html' }); + }); + + inject(function ($route, $location, $rootScope) { + $location.path('/foo'); + expect(function() { + $rootScope.$digest(); + }).toThrow('[$sce:isecrurl] Blocked loading resource from url not allowed by $sceDelegate policy. URL: http://example.com/foo.html'); + }); + }); + + it('should load cross domain templates that are trusted', function() { + module(function($routeProvider, $sceDelegateProvider) { + $routeProvider.when('/foo', { templateUrl: 'http://example.com/foo.html' }); + $sceDelegateProvider.resourceUrlWhitelist([/^http:\/\/example\.com\/foo\.html$/]); + }); + + inject(function ($route, $location, $rootScope) { + $httpBackend.whenGET('http://example.com/foo.html').respond('FOO BODY'); + $location.path('/foo'); + $rootScope.$digest(); + $httpBackend.flush(); + expect($route.current.locals.$template).toEqual('FOO BODY'); + }); + }); it('should not update $routeParams until $routeChangeSuccess', function() { module(function($routeProvider) { @@ -904,7 +932,7 @@ describe('$route', function() { return '

' + routePathParams.id + '

'; } - module(function($routeProvider){ + module(function($routeProvider) { $routeProvider.when('/bar/:id/:subid/:subsubid', {templateUrl: 'bar.html'}); $routeProvider.when('/foo/:id', {template: customTemplateFn}); }); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 5fae28179a27..d97d88a104ce 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -107,7 +107,12 @@ function dealoc(obj) { function cleanup(element) { element.off().removeData(); - for ( var i = 0, children = element.contents() || []; i < children.length; i++) { + // Note: We aren't using element.contents() here. Under jQuery, element.contents() can fail + // for IFRAME elements. jQuery explicitly uses (element.contentDocument || + // element.contentWindow.document) and both properties are null for IFRAMES that aren't attached + // to a document. + var children = element[0].childNodes || []; + for ( var i = 0; i < children.length; i++) { cleanup(angular.element(children[i])); } }