Skip to content
This repository has been archived by the owner on Aug 29, 2023. It is now read-only.

fix(select): block xss on md-select-label #10023

Merged
merged 1 commit into from Dec 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/components/select/select.js
Expand Up @@ -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.
Expand Down Expand Up @@ -174,7 +179,8 @@ angular.module('material.components.select', [
* </div>
* </hljs>
*/
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];

Expand Down Expand Up @@ -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 <md-option>
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

} 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) {
Expand Down
67 changes: 66 additions & 1 deletion src/components/select/select.spec.js
Expand Up @@ -3,7 +3,7 @@ describe('<md-select>', 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');
Expand Down Expand Up @@ -437,6 +437,32 @@ describe('<md-select>', function() {
expect(label.text()).toBe($rootScope.selectedText);
});

it('should sanitize md-selected-html', function() {
$rootScope.selectedText = '<b>Hello World</b><script>window.mdSelectXss="YES"</script>';

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 '<md-select-value>`.
expect(label[0].childNodes[0].innerHTML).toBe('<b>Hello World</b>');
expect(window.mdSelectXss).toBeUndefined();
});

it('should always treat md-selected-text as text, not html', function() {
$rootScope.selectedText = '<b>Hello World</b>';

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('<b>Hello World</b>');
});

it('supports rendering multiple', function() {
$rootScope.val = [1, 3];
var select = $compile('<md-input-container>' +
Expand Down Expand Up @@ -1379,3 +1405,42 @@ describe('<md-select>', function() {
}

});

describe('<md-select> 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 =
'<md-select md-selected-html="myHtml" ng-model="selectedValue">' +
'<md-option>One</md-option>' +
'</md-select>';

var select = $compile(template)(pageScope);

expect(function() {
pageScope.myHtml = '<p>Barnacle Pete</p>';
pageScope.$apply();
}).toThrowError(/\$sce:unsafe/);
});


it('should throw an error if using md-selected-text and md-selected-html', function() {
var template =
'<md-select md-selected-text="myText" md-selected-html="myHtml" ng-model="selectedValue">' +
'<md-option>One</md-option>' +
'</md-select>';

var select = $compile(template)(pageScope);

expect(function() {
pageScope.$apply();
}).toThrowError('md-select cannot have both `md-selected-text` and `md-selected-html`');
});
});