2
\$\begingroup\$

Sometimes, we have to do some fixing to insert a string into an HTML property. Or just to display it.

But not everything is safe! Consider the following example:

document.getElementById('name').innerHTML = prompt('What is your name?');

This allows to introduce HTML vulnerabilities quite easily. Specially when you want to do HTML > X/HTML conversions.

So, I've came up with the following:

var safetext = function(text){
    var div = document.createElement('div');
    div.innerText = div.textContent = text;
    var safetext = div.innerHTML;
    div = null;
    return safetext.replace(/^\s+|\s+$/g, '');
};

But then I realized that it could be optimized a little. That div is created and destroyed every single time you need a cleanup. So, I've optimized to the following:

var safetext = (function(text){
    this.innerText = this.textContent = text;
    return this.innerHTML.replace(/^\s+|\s+$/g, '');
}).bind(document.createElement('div'));

The this will always be the div we created, and will always be the same one, so, no unnecessary cleanups or anything. It is always kept in memory, since it is bound to the this in the anonymous function. Or isn't it?

The resulting text is a string that is safe to use in your HTML. Using the example from before:

document.getElementById('name').innerHTML = safetext(prompt('What is your name?'));

This should show &lt;script&gt; if your name (for some bizarre reason) is <script>. Or it leaves unsafe characters behind, bound to break everything?


To the best of my knowledge, and based on the 200 tests where I've used it, I didn't had a single problem or hiccup.

Am I missing something? Is my code indeed returning safe HTML strings? Is the 2nd iteration really more performant?

\$\endgroup\$
6
  • \$\begingroup\$ As a general rule, if you have to ask "is this safe?" then it's not. It may cover all the attack vectors you can currently think of, but there a million more you haven't. The best solution is, in general, "Don't roll your own". Go find a widely used, extensively tested library/function/etc. and implement that. \$\endgroup\$
    – Kaz
    Commented Apr 14, 2016 at 9:37
  • \$\begingroup\$ @Zak For the purpose, it is quite overkill to use an entire library. Specially when all I wan't to make sure is that a client doesn't write <this> and break the whole output. I'm re-implementing an HTML > Excel XML convertion function. Also, I haven't found any library, so far, to do this. Most of what I've found is a .replace with a huge object with all entities, some weird replace or some close form of what I've presented (which I wasn't aware that is very used!). If you know of any library, I'm glad to check it. \$\endgroup\$ Commented Apr 14, 2016 at 9:47
  • 1
    \$\begingroup\$ It is almost never overkill when it comes to security measures in code and sanitising inputs. \$\endgroup\$
    – Dan
    Commented Apr 14, 2016 at 9:54
  • \$\begingroup\$ @DanPantry You're right. But really, I have no idea about any library that does this kind of stuff. That is, in Javascript. In PHP, there's tons of them. Do you know any library? \$\endgroup\$ Commented Apr 14, 2016 at 10:00
  • \$\begingroup\$ Unfortunately not any that would work from a CDN \$\endgroup\$
    – Dan
    Commented Apr 14, 2016 at 10:04

2 Answers 2

2
\$\begingroup\$

The Attack

What you are basically talking about is DOM based XSS.

But it's a self-XSS, so the only possible attack vector might be via ClickJacking (depending on the used browser).

Still, it should be defended against, especially for usability reasons.

Proper Defense

Here is OWASPs guide on preventing DOM based XSS. Basically, you should first HTML encode the input, and then JavaScript encode.

For your example, the second part doesn't seem necessary, just encoding <, >, ' and " should be enough.

Your Defense

Well, your defense works.

It could be simplified a bit though. There really doesn't seem to be a need to set this.textContent, and I also do not see the need for replace(/^\s+|\s+$/g, '') (or am I missing something here? Browser compatibility maybe?).

That would leave you with

var safetext = (function(text){
    this.textContent = text;
    return this.innerHTML;
}).bind(document.createElement('div'));

The reason this works seems to be the way textContent works.

But this really seems to be more of a side-effect, and it is never a good idea to use those for security related things.

Conclusion

Regarding performance, security, and readability, a simple replace for <, >, ' and " with their HTML representation seems to be preferable to your method.

\$\endgroup\$
1
  • \$\begingroup\$ Yup, you are missing something. this.innerText is for Internet Explorer and Google Chrome while this.textContent is for Firefox and Google Chrome. I don't know where Microsoft Edge sits on this. The replace(/^\s+|\s+$/g, '') is a simple trim, for Internet Explorer. I'm not sure if Internet Explorer 9 has String.trim, but Internet Explorer 8 doesn't. The result of this function is used on XML, which requires & to be encoded to &amp; as well. HTML doesn't like it, but won't break. \$\endgroup\$ Commented Apr 14, 2016 at 13:35
1
\$\begingroup\$

Adding to the amazing answer given by @tim, I have to add a "bug" I have found on it.

There is a somewhat desirable side-effect to this code that makes it unusable for XML: every newline is replaced with a <br/>.

XML is very picky about the syntax and a stray <br/> showing up will be a very nasty game-breaker.


You can see what I mean by running this example:

var safetext = (function(text){
    this.innerText = this.textContent = text;
    return this.innerHTML.replace(/^\s+|\s+$/g, '');
}).bind(document.createElement('div'));

alert(safetext('A newline: \n see?'));

This won't happen if I do it the way @tim suggests.

A better implementation would be like this:

var safetext = function(text){
	var table = {
		'<': 'lt',
		'>': 'gt',
		'"': 'quot',
		'\'': 'apos',
		'&': 'amp',
		'\r': '#10',
		'\n': '#13'
	};
	
	return text.toString().replace(/[<>"'\r\n&]/g, function(chr){
		return '&' + table[chr] + ';';
	});
};

alert(safetext('A newline: \n see?'));

This doesn't have the "bug" and it works in XML and HTML.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.