Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 0b7da54

Browse files
committed
fix(select): block xss on md-select-label
1 parent 088d2e6 commit 0b7da54

File tree

2 files changed

+101
-8
lines changed

2 files changed

+101
-8
lines changed

src/components/select/select.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ angular.module('material.components.select', [
7575
* @param {expression=} md-on-open Expression to be evaluated when opening the select.
7676
* Will hide the select options and show a spinner until the evaluated promise resolves.
7777
* @param {expression=} md-selected-text Expression to be evaluated that will return a string
78-
* to be displayed as a placeholder in the select input box when it is closed.
78+
* to be displayed as a placeholder in the select input box when it is closed. The value
79+
* will be treated as *text* (not html).
80+
* @param {expression=} md-selected-html Expression to be evaluated that will return a string
81+
* to be displayed as a placeholder in the select input box when it is closed. The value
82+
* will be treated as *html*. The value must either be explicitly marked as trustedHtml or
83+
* the ngSanitize module must be loaded.
7984
* @param {string=} placeholder Placeholder hint text.
8085
* @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the
8186
* floating label. **Note:** This attribute is only evaluated once; it is not watched.
@@ -174,7 +179,8 @@ angular.module('material.components.select', [
174179
* </div>
175180
* </hljs>
176181
*/
177-
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse) {
182+
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
183+
$injector) {
178184
var keyCodes = $mdConstant.KEY_CODE;
179185
var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
180186

@@ -337,17 +343,39 @@ function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $
337343
mdSelectCtrl.setLabelText = function(text) {
338344
mdSelectCtrl.setIsPlaceholder(!text);
339345

340-
if (attr.mdSelectedText) {
341-
text = $parse(attr.mdSelectedText)(scope);
342-
} else {
346+
// Whether the select label has been given via user content rather than the internal
347+
// template of <md-option>
348+
var isSelectLabelFromUser = false;
349+
350+
if (attr.mdSelectedText && attr.mdSelectedHtml) {
351+
throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
352+
}
353+
354+
if (attr.mdSelectedText || attr.mdSelectedHtml) {
355+
text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
356+
isSelectLabelFromUser = true;
357+
} else if (!text) {
343358
// Use placeholder attribute, otherwise fallback to the md-input-container label
344359
var tmpPlaceholder = attr.placeholder ||
345360
(containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
346-
text = text || tmpPlaceholder || '';
361+
362+
text = tmpPlaceholder || '';
363+
isSelectLabelFromUser = true;
347364
}
348365

349366
var target = valueEl.children().eq(0);
350-
target.html(text);
367+
368+
if (attr.mdSelectedHtml) {
369+
// Using getTrustedHtml will run the content through $sanitize if it is not already
370+
// explicitly trusted. If the ngSanitize module is not loaded, this will
371+
// *correctly* throw an sce error.
372+
target.html($sce.getTrustedHtml(text));
373+
} else if (isSelectLabelFromUser) {
374+
target.text(text);
375+
} else {
376+
// If we've reached this point, the text is not user-provided.
377+
target.html(text);
378+
}
351379
};
352380

353381
mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {

src/components/select/select.spec.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ describe('<md-select>', function() {
33
var body, $document, $rootScope, $compile, $timeout, $material;
44

55
beforeEach(function() {
6-
module('material.components.select', 'material.components.input');
6+
module('material.components.select', 'material.components.input', 'ngSanitize');
77

88
inject(function($injector) {
99
$document = $injector.get('$document');
@@ -437,6 +437,32 @@ describe('<md-select>', function() {
437437
expect(label.text()).toBe($rootScope.selectedText);
438438
});
439439

440+
it('should sanitize md-selected-html', function() {
441+
$rootScope.selectedText = '<b>Hello World</b><script>window.mdSelectXss="YES"</script>';
442+
443+
var select = setupSelect(
444+
'ng-model="someVal", ' +
445+
'md-selected-html="selectedText"', null, true).find('md-select');
446+
var label = select.find('md-select-value');
447+
448+
expect(label.text()).toBe('Hello World');
449+
450+
// The label is loaded into a span that is the first child of the '<md-select-value>`.
451+
expect(label[0].childNodes[0].innerHTML).toBe('<b>Hello World</b>');
452+
expect(window.mdSelectXss).toBeUndefined();
453+
});
454+
455+
it('should always treat md-selected-text as text, not html', function() {
456+
$rootScope.selectedText = '<b>Hello World</b>';
457+
458+
var select = setupSelect(
459+
'ng-model="someVal", ' +
460+
'md-selected-text="selectedText"', null, true).find('md-select');
461+
var label = select.find('md-select-value');
462+
463+
expect(label.text()).toBe('<b>Hello World</b>');
464+
});
465+
440466
it('supports rendering multiple', function() {
441467
$rootScope.val = [1, 3];
442468
var select = $compile('<md-input-container>' +
@@ -1379,3 +1405,42 @@ describe('<md-select>', function() {
13791405
}
13801406

13811407
});
1408+
1409+
describe('<md-select> without ngSanitize loaded', function() {
1410+
var $compile, pageScope;
1411+
1412+
beforeEach(module('material.components.select', 'material.components.input'));
1413+
1414+
beforeEach(inject(function($injector) {
1415+
$compile = $injector.get('$compile');
1416+
pageScope = $injector.get('$rootScope').$new();
1417+
}));
1418+
1419+
it('should throw an error when using md-selected-html without ngSanitize', function() {
1420+
var template =
1421+
'<md-select md-selected-html="myHtml" ng-model="selectedValue">' +
1422+
'<md-option>One</md-option>' +
1423+
'</md-select>';
1424+
1425+
var select = $compile(template)(pageScope);
1426+
1427+
expect(function() {
1428+
pageScope.myHtml = '<p>Barnacle Pete</p>';
1429+
pageScope.$apply();
1430+
}).toThrowError(/\$sce:unsafe/);
1431+
});
1432+
1433+
1434+
it('should throw an error if using md-selected-text and md-selected-html', function() {
1435+
var template =
1436+
'<md-select md-selected-text="myText" md-selected-html="myHtml" ng-model="selectedValue">' +
1437+
'<md-option>One</md-option>' +
1438+
'</md-select>';
1439+
1440+
var select = $compile(template)(pageScope);
1441+
1442+
expect(function() {
1443+
pageScope.$apply();
1444+
}).toThrowError('md-select cannot have both `md-selected-text` and `md-selected-html`');
1445+
});
1446+
});

0 commit comments

Comments
 (0)