From 0b7da547e4cfd378fc826a1557a09b9b0c42145d Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Tue, 15 Nov 2016 15:06:21 -0800 Subject: [PATCH] fix(select): block xss on md-select-label --- src/components/select/select.js | 42 ++++++++++++++--- src/components/select/select.spec.js | 67 +++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/src/components/select/select.js b/src/components/select/select.js index ad766dd2754..f8b727354cf 100755 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -75,7 +75,12 @@ angular.module('material.components.select', [ * @param {expression=} md-on-open Expression to be evaluated when opening the select. * Will hide the select options and show a spinner until the evaluated promise resolves. * @param {expression=} md-selected-text Expression to be evaluated that will return a string - * to be displayed as a placeholder in the select input box when it is closed. + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *text* (not html). + * @param {expression=} md-selected-html Expression to be evaluated that will return a string + * to be displayed as a placeholder in the select input box when it is closed. The value + * will be treated as *html*. The value must either be explicitly marked as trustedHtml or + * the ngSanitize module must be loaded. * @param {string=} placeholder Placeholder hint text. * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the * floating label. **Note:** This attribute is only evaluated once; it is not watched. @@ -174,7 +179,8 @@ angular.module('material.components.select', [ * * */ -function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse) { +function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce, + $injector) { var keyCodes = $mdConstant.KEY_CODE; var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW]; @@ -337,17 +343,39 @@ function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $ mdSelectCtrl.setLabelText = function(text) { mdSelectCtrl.setIsPlaceholder(!text); - if (attr.mdSelectedText) { - text = $parse(attr.mdSelectedText)(scope); - } else { + // Whether the select label has been given via user content rather than the internal + // template of + var isSelectLabelFromUser = false; + + if (attr.mdSelectedText && attr.mdSelectedHtml) { + throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`'); + } + + if (attr.mdSelectedText || attr.mdSelectedHtml) { + text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope); + isSelectLabelFromUser = true; + } else if (!text) { // Use placeholder attribute, otherwise fallback to the md-input-container label var tmpPlaceholder = attr.placeholder || (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : ''); - text = text || tmpPlaceholder || ''; + + text = tmpPlaceholder || ''; + isSelectLabelFromUser = true; } var target = valueEl.children().eq(0); - target.html(text); + + if (attr.mdSelectedHtml) { + // Using getTrustedHtml will run the content through $sanitize if it is not already + // explicitly trusted. If the ngSanitize module is not loaded, this will + // *correctly* throw an sce error. + target.html($sce.getTrustedHtml(text)); + } else if (isSelectLabelFromUser) { + target.text(text); + } else { + // If we've reached this point, the text is not user-provided. + target.html(text); + } }; mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) { diff --git a/src/components/select/select.spec.js b/src/components/select/select.spec.js index 0193b5f19e9..c6528b54d69 100755 --- a/src/components/select/select.spec.js +++ b/src/components/select/select.spec.js @@ -3,7 +3,7 @@ describe('', function() { var body, $document, $rootScope, $compile, $timeout, $material; beforeEach(function() { - module('material.components.select', 'material.components.input'); + module('material.components.select', 'material.components.input', 'ngSanitize'); inject(function($injector) { $document = $injector.get('$document'); @@ -437,6 +437,32 @@ describe('', function() { expect(label.text()).toBe($rootScope.selectedText); }); + it('should sanitize md-selected-html', function() { + $rootScope.selectedText = 'Hello World'; + + var select = setupSelect( + 'ng-model="someVal", ' + + 'md-selected-html="selectedText"', null, true).find('md-select'); + var label = select.find('md-select-value'); + + expect(label.text()).toBe('Hello World'); + + // The label is loaded into a span that is the first child of the '`. + expect(label[0].childNodes[0].innerHTML).toBe('Hello World'); + expect(window.mdSelectXss).toBeUndefined(); + }); + + it('should always treat md-selected-text as text, not html', function() { + $rootScope.selectedText = 'Hello World'; + + var select = setupSelect( + 'ng-model="someVal", ' + + 'md-selected-text="selectedText"', null, true).find('md-select'); + var label = select.find('md-select-value'); + + expect(label.text()).toBe('Hello World'); + }); + it('supports rendering multiple', function() { $rootScope.val = [1, 3]; var select = $compile('' + @@ -1379,3 +1405,42 @@ describe('', function() { } }); + +describe(' without ngSanitize loaded', function() { + var $compile, pageScope; + + beforeEach(module('material.components.select', 'material.components.input')); + + beforeEach(inject(function($injector) { + $compile = $injector.get('$compile'); + pageScope = $injector.get('$rootScope').$new(); + })); + + it('should throw an error when using md-selected-html without ngSanitize', function() { + var template = + '' + + 'One' + + ''; + + var select = $compile(template)(pageScope); + + expect(function() { + pageScope.myHtml = '

Barnacle Pete

'; + pageScope.$apply(); + }).toThrowError(/\$sce:unsafe/); + }); + + + it('should throw an error if using md-selected-text and md-selected-html', function() { + var template = + '' + + 'One' + + ''; + + var select = $compile(template)(pageScope); + + expect(function() { + pageScope.$apply(); + }).toThrowError('md-select cannot have both `md-selected-text` and `md-selected-html`'); + }); +});