Saturday, March 25, 2017

True Tales of XSS: jQuery's text() Function

A simplified version of the code:
<div id="foo">
<?php echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8'); ?>
</div>

<script>
var bar = $('#foo').text();
$('#foo').html('<b>' + bar + '</b>');
</script>

Overview: user-supplied $input is echoed after having HTML entities encoded. Next, it's read by jQuery's text(), some decorative tags are added, and written back with html(). Although there's usually little reason to do this in jQuery, it doesn't appear to be vulnerable to XSS.

And yet, it is.

1. We feed a standard XSS test to $input:
<script>alert(1);</script>
2. The $input is encoded with htmlspecialchars(), effectively preventing our XSS from functioning:
 &lt;script&gt;alert(1);&lt;/script&gt;
 3. Our encoded $input is read by jQuery's text() method, which should remove tags and only return the text contents of HTML elements. So, at this point, we could assume our $input would still be:
&lt;script&gt;alert(1);&lt;/script&gt;
Or, if it also removes encoded tags, possibly:
alert(1);
However, text() actually decodes HTML entities and will happily return valid HTML, restoring our $input to its original state:
<script>alert(1);</script>
4. Decorative formatting tags are added to the $input:
<b><script>alert(1);</script></b>
5. The $input is written back to the page with html(), which will also execute our script tag and launch our test payload.

So, keep an eye out for the next time you see text() handling user input, XSS Rangers.