diff --git a/src/components/icon/icon.spec.js b/src/components/icon/icon.spec.js
index 5bd4d1e20aa..74e40b42c6c 100644
--- a/src/components/icon/icon.spec.js
+++ b/src/components/icon/icon.spec.js
@@ -1,12 +1,11 @@
describe('MdIcon directive', function() {
- var el;
- var $scope;
- var $compile;
- var $mdIconProvider;
+ var el, $scope, $compile, $mdIconProvider, $sce;
+ var wasLastSvgSrcTrusted = false;
beforeEach(module('material.components.icon', function(_$mdIconProvider_) {
$mdIconProvider = _$mdIconProvider_;
}));
+
afterEach(function() {
$mdIconProvider.defaultFontSet('material-icons');
$mdIconProvider.fontSet('fa', 'fa');
@@ -172,6 +171,13 @@ describe('MdIcon directive', function() {
module(function($provide) {
var $mdIconMock = function(id) {
+
+ wasLastSvgSrcTrusted = false;
+ if (!angular.isString(id)) {
+ id = $sce.getTrustedUrl(id);
+ wasLastSvgSrcTrusted = true;
+ }
+
return {
then: function(fn) {
switch(id) {
@@ -183,6 +189,8 @@ describe('MdIcon directive', function() {
break;
case 'cake.svg' : fn('');
break;
+ case 'galactica.svg' : fn('');
+ break;
case 'image:android' : fn('');
break;
default :
@@ -229,10 +237,21 @@ describe('MdIcon directive', function() {
describe('using md-svg-src=""', function() {
+ beforeEach(inject(function(_$sce_) {
+ $sce = _$sce_;
+ }));
+
+ it('should mark as trusted static URLs', function() {
+ el = make('');
+ expect(wasLastSvgSrcTrusted).toBe(true);
+ expect(el[0].innerHTML).toContain('galactica')
+ });
+
it('should update mdSvgSrc when attribute value changes', function() {
$scope.url = 'android.svg';
el = make('');
expect(el.attr('md-svg-src')).toEqual('android.svg');
+ expect(wasLastSvgSrcTrusted).toBe(false);
$scope.url = 'cake.svg';
$scope.$digest();
expect(el.attr('md-svg-src')).toEqual('cake.svg');
diff --git a/src/components/icon/js/iconDirective.js b/src/components/icon/js/iconDirective.js
index 3615fc75b5f..c028906c0b0 100644
--- a/src/components/icon/js/iconDirective.js
+++ b/src/components/icon/js/iconDirective.js
@@ -1,6 +1,6 @@
angular
.module('material.components.icon')
- .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', mdIconDirective]);
+ .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', '$sce', mdIconDirective]);
/**
* @ngdoc directive
@@ -171,7 +171,7 @@ angular
*
*
*/
-function mdIconDirective($mdIcon, $mdTheming, $mdAria ) {
+function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
return {
restrict: 'E',
@@ -191,6 +191,10 @@ function mdIconDirective($mdIcon, $mdTheming, $mdAria ) {
// If using a font-icon, then the textual name of the icon itself
// provides the aria-label.
+ // Keep track of the content of the svg src so we can compare against it later to see if the
+ // attribute is static (and thus safe).
+ var originalSvgSrc = element.attr(attr.$attr.mdSvgSrc);
+
var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text();
var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
@@ -213,6 +217,12 @@ function mdIconDirective($mdIcon, $mdTheming, $mdAria ) {
// Use either pre-configured SVG or URL source, respectively.
attr.$observe(attrName, function(attrVal) {
+ // If using svg-src and the value is static (i.e., is exactly equal to the compile-time
+ // `md-svg-src` value), then it is implicitly trusted.
+ if (!isInlineSvg(attrVal) && attrVal === originalSvgSrc) {
+ attrVal = $sce.trustAsUrl(attrVal);
+ }
+
element.empty();
if (attrVal) {
$mdIcon(attrVal)
@@ -245,4 +255,14 @@ function mdIconDirective($mdIcon, $mdTheming, $mdAria ) {
}
}
}
+
+ /**
+ * Gets whether the given svg src is an inline ("data:" style) SVG.
+ * @param {string} svgSrc The svg src.
+ * @returns {boolean} Whether the src is an inline SVG.
+ */
+ function isInlineSvg(svgSrc) {
+ var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;
+ return dataUrlRegex.test(svgSrc);
+ }
}
diff --git a/src/components/icon/js/iconService.js b/src/components/icon/js/iconService.js
index 986113cc22a..f3d87725819 100644
--- a/src/components/icon/js/iconService.js
+++ b/src/components/icon/js/iconService.js
@@ -358,9 +358,9 @@ MdIconProvider.prototype = {
},
- $get: ['$templateRequest', '$q', '$log', '$templateCache', '$mdUtil', function($templateRequest, $q, $log, $templateCache, $mdUtil) {
+ $get: ['$templateRequest', '$q', '$log', '$templateCache', '$mdUtil', '$sce', function($templateRequest, $q, $log, $templateCache, $mdUtil, $sce) {
this.preloadIcons($templateCache);
- return MdIconService(config, $templateRequest, $q, $log, $mdUtil);
+ return MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce);
}]
};
@@ -415,7 +415,7 @@ function ConfigurationItem(url, viewBoxSize) {
*/
/* @ngInject */
-function MdIconService(config, $templateRequest, $q, $log, $mdUtil) {
+function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
var iconCache = {};
var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i;
var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;
@@ -432,12 +432,28 @@ function MdIconService(config, $templateRequest, $q, $log, $mdUtil) {
function getIcon(id) {
id = id || '';
+ // If the "id" provided is not a string, the only other valid value is a $sce trust wrapper
+ // over a URL string. If the value is not trusted, this will intentionally throw an error
+ // because the user is attempted to use an unsafe URL, potentially opening themselves up
+ // to an XSS attack.
+ if (!angular.isString(id)) {
+ id = $sce.getTrustedUrl(id);
+ }
+
// If already loaded and cached, use a clone of the cached icon.
// Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
- if (iconCache[id]) return $q.when(transformClone(iconCache[id]));
- if (urlRegex.test(id) || dataUrlRegex.test(id)) return loadByURL(id).then(cacheIcon(id));
- if (id.indexOf(':') == -1) id = '$default:' + id;
+ if (iconCache[id]) {
+ return $q.when(transformClone(iconCache[id]));
+ }
+
+ if (urlRegex.test(id) || dataUrlRegex.test(id)) {
+ return loadByURL(id).then(cacheIcon(id));
+ }
+
+ if (id.indexOf(':') == -1) {
+ id = '$default:' + id;
+ }
var load = config[id] ? loadByID : loadFromIconSet;
return load(id)
@@ -540,9 +556,7 @@ function MdIconService(config, $templateRequest, $q, $log, $mdUtil) {
/* Load the icon by URL using HTTP. */
function loadByHttpUrl(url) {
return $q(function(resolve, reject) {
- /**
- * Catch HTTP or generic errors not related to incorrect icon IDs.
- */
+ // Catch HTTP or generic errors not related to incorrect icon IDs.
var announceAndReject = function(err) {
var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
$log.warn(msg);