Propperly escape option value to prevent xss

When I create a select menu with options I use php’s htmlspecialchar() to escape the visible value of an option.

<option value="xxx"><?= htmlspecialchars('<script>alert("name")</script>') ?></option>

This is sufficient to prevent the execution of the js for the currently selected item, but the items in the drop down menu has the executable js code. So, as soon as the drop down with the menu options opens, the js code is executed. Any way to fix this?

Can you show me an example of some actual rendered option values in your Select2? Select2 has built in XSS protection, so you should not need to escape values in your <select> (or, if you’re using a remote data source, that data).

I tried the following. The alert is still executed.

<html>
<head>
<link href="styles/bootstrap/select2/css/select2.min.css" rel="stylesheet">
<script src="styles/bootstrap/jquery/jquery.min.js"></script>
<script src="styles/bootstrap/select2/js/select2.min.js"></script>
<script>
$(document).ready( function() {
  $(".chzn-select").select2({
    width: '100%',
  });
});
</script>
</head><body>
<select class="chzn-select">
  <option value="4"><script>alert("name")</script></option>
</select>
</body>
</html>

In that case you can write a custom templateResult callback to control the output more directly. The documentation above also discusses how the output from your custom render function is escaped by Select2 (and how to override this). You may also need a templateSelection callback that does the same thing, since the selected item will be rendered in the Select2’s “collapsed” state.

I tried templateResult, but still the js code is executed when the drop down menu is opened. Even if I escape the option text. Have you got another idea?

<html>
<head>
<link href="styles/bootstrap/select2/css/select2.min.css" rel="stylesheet">
<script src="styles/bootstrap/jquery/jquery.min.js"></script>
<script src="styles/bootstrap/select2/js/select2.min.js"></script>
<script>
chzn_template_func =  function (state) {
  var html = '<span>'+state.text+'';
  html += '</span>';
  return $(html);
};

$(document).ready( function() {
  $(".chzn-select").select2({
    width: '100%',
    templateResult: chzn_template_func
  });
});
</script>
</head>
<body>
<select class="chzn-select">
  <option value="4"><?= htmlspecialchars('<script>alert("name")</script>') ?></option>
</select>
</body>
</html>

That’s because you returned the item’s text in a jQuery HTML structure. The documentation on Select2’s built-in escaping states that if you do that, Select2 will not try to sanitize the output—it expects you to do that yourself. You have two choices for your templateResult callback:

  1. Return only the text of the item (e.g., return state.text;); however, this will completely remove your HTML tags (which I don’t think is what you want).
  2. Escape the item’s value yourself within the jQuery object: (e.g., return $('<span>' + state.text.replace(/</, '&lt;'/g) + '</span>');). Either of these approaches should prevent the code in the option’s value from being executed by the browser.

Also, as I noted in a previous response, that value is displayed in two places: the dropdown, and (when selected) the selection/search box). templateResult only handles the display of items in the dropdown. You will need to apply the same callback to templateSelection to escape the value when it is displayed in the selection box.

(Note: it looks like you’re using the PHP function htmlspecialchars in your HTML output, so that option’s value should already be escaped before Select2 gets it. Can you confirm that the escaped value is actually being written into the HTML? Because if it is, then you shouldn’t need the templateResult function at all.)

Now I got it! I first thought that it would be sufficient to use php’s htmlspecialchars() on each option, even if Select2 doesn’t sanitize the output, but htmlspecialchars() is not enough. The replace() is also needed. Just using replace() without htmlspecialchars() isn’t sufficient either, unless you don’t apply the templateResult and templateSelection callback (just as you said). Unfortunately my original use case did require the callbacks, because additional data was passed in the data attributes of each option which was combined with the text from the option. Here is my final code which can handle both

<html>
<head>
<link href="styles/bootstrap/select2/css/select2.min.css" rel="stylesheet">
<script src="styles/bootstrap/jquery/jquery.min.js"></script>
<script src="styles/bootstrap/select2/js/select2.min.js"></script>
<script>
chzn_template_func =  function (state) {
  var subtitle='';
  if($(state.element).data('subtitle'))
    subtitle = $(state.element).data('subtitle');
  return $('<span>'+state.text.replace(/</g, '&lt;')+'<br /><i>'+subtitle.replace(/</g, '&lt;')+'</i></span>');
};

$(document).ready( function() {
  $(".chzn-select").select2({
    width: '100%',
    templateResult: chzn_template_func,
    templateSelection: chzn_template_func
  });
});
</script>
</head>
<body>
<select class="chzn-select">
  <option value="4" data-subtitle="<?= htmlspecialchars('<script>alert("more xss")</script>') ?>"><?= htmlspecialchars('<script>alert("name")</script>') ?></option>
</select>
</body>
</html> 

Many thanks for your help.

I’m guessing that when you use htmlspecialcharacters() on the <option> value that Select2 interprets the escapes as what they replaced, and then your templateResult function sticks that text (now “literal” HTML) into a jQuery object, which Select2 won’t escape. So the replace() inside the templateResult function is what’s doing the actual escaping, and you could probably just get rid of htmlspecialcharacters() altogether.

In any case, I’m glad you got it working!

The htmlspecialchars() is still needed, because otherwise I end up with html code like

<option value="4" data-subtitle="<script>alert('more xss')</script>"><script>alert("name")</script></option>

which triggers the XSS. I don’t have to run htmlspecialchars() on the data-attribute, because that isn’t executed by they browser but the other script is executed.

Something I didn’t expected was, that despite of the htmlspecialchar() escaping, the callbacks receive an unescapted html string, otherwise the replacing of ‘<’ into ‘&lt;’ wouldn’t work. So, there must be some unescaping on the way to the callback. Hence, the htmlspecialchar() calls are somewhat superfluous at least for an XSS safe Select2, but not for XSS safe html code (see above).

Hope this helps other users.

Ok, yeah. I overlooked that you’re injecting the html code into the select.