1
votes

Context

I have a website where users can write their own articles. I use a contenteditable div that allows users to mark up their HTML (bold, italicize, etc.), and I want to make sure I prevent XSS attacks. To do so, I use htmlspecialchars() when outputting data onto the page. I want to leave the user's input data alone and encode it on output. However, I use the .html() method to get the HTML content in the contenteditable div, and it seems that user-inserted tags are automatically encoded by the .html() method or the contenteditable div.

The structure of the contenteditable div is

<div class="article" contenteditable="true"></div>

As an example, if the user attempts to do XSS on the contenteditable div by inserting <script>alert('hi')</script>, like so:

<div class="article" contenteditable="true">
    <script>alert('hi')</script>
</div>

When I submit the data to the database (using var article = $(".article").html();), the data I see in the database isn't <script>alert('hi')</script>, but is instead &lt;script&gt;alert('hi')&lt;/script&gt; despite the fact that I didn't encode anything on my own. Then, I have to avoid using htmlspecialchars() when outputting the article content so that the div encodes the string back to its original form instead of staying encoded.

Question

Does the .html() method automatically encode HTML, and if so, how would one prevent this functionality (if that is advisable)?

1
console.log would answer this for you - GetSet
@GetSet Good point. It does result in the same encoded characters. Do you know of any way to prevent this? Maybe I should revert it in the server-side code. - Carson D
You could use a javascript function (sadly, not in-built, or is it these days? dunno) to do a "htmlchars decode". I could post it as an answer, although it's not my code. And the link to it has a new and "improved" way of doing it, but I like the old way and thus would post it as an answer, if you are interested. - GetSet
Btw, on Maybe I should revert it in the server-side code, PHP does have their htmlspecialchars_decode() so thats a good idea too. The data has to go server-side anyway for storage correct? You could return the data decoded after your server does it's business. - GetSet
@GetSet Yeah, I would take that answer! I would also mention that .html() does encode HTML? Thanks for the response. - Carson D

1 Answers

0
votes

Ok Carson D, The following code has benefited me in my travels.

Here's the source link. As mentioned, they may have updated it to modern approaches. If you want just a stand-alone function then remove the module.exports = bit from the code in the link as follows:

https://locutus.io/php/strings/htmlspecialchars_decode/

It is possible that that link may become stale in SO's future history, so here is that code in code form with all credits given due. This is the code I used since at least 2013/14 so it may not indeed match the code in the link, but I believe the authors are the same.

function htmlspecialchars_decode (string, quoteStyle) { // eslint-disable-line camelcase
  //       discuss at: https://locutus.io/php/htmlspecialchars_decode/
  //      original by: Mirek Slugen
  //      improved by: Kevin van Zonneveld (https://kvz.io)
  //      bugfixed by: Mateusz "loonquawl" Zalega
  //      bugfixed by: Onno Marsman (https://twitter.com/onnomarsman)
  //      bugfixed by: Brett Zamir (https://brett-zamir.me)
  //      bugfixed by: Brett Zamir (https://brett-zamir.me)
  //         input by: ReverseSyntax
  //         input by: Slawomir Kaniecki
  //         input by: Scott Cariss
  //         input by: Francois
  //         input by: Ratheous
  //         input by: Mailfaker (https://www.weedem.fr/)
  //       revised by: Kevin van Zonneveld (https://kvz.io)
  // reimplemented by: Brett Zamir (https://brett-zamir.me)
  //        example 1: htmlspecialchars_decode("<p>this -&gt; &quot;</p>", 'ENT_NOQUOTES')
  //        returns 1: '<p>this -> &quot;</p>'
  //        example 2: htmlspecialchars_decode("&amp;quot;")
  //        returns 2: '&quot;'

  var optTemp = 0
  var i = 0
  var noquotes = false

  if (typeof quoteStyle === 'undefined') {
    quoteStyle = 2
  }
  string = string.toString()
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
  var OPTS = {
    'ENT_NOQUOTES': 0,
    'ENT_HTML_QUOTE_SINGLE': 1,
    'ENT_HTML_QUOTE_DOUBLE': 2,
    'ENT_COMPAT': 2,
    'ENT_QUOTES': 3,
    'ENT_IGNORE': 4
  }
  if (quoteStyle === 0) {
    noquotes = true
  }
  if (typeof quoteStyle !== 'number') {
    // Allow for a single string or an array of string flags
    quoteStyle = [].concat(quoteStyle)
    for (i = 0; i < quoteStyle.length; i++) {
      // Resolve string input to bitwise e.g. 'PATHINFO_EXTENSION' becomes 4
      if (OPTS[quoteStyle[i]] === 0) {
        noquotes = true
      } else if (OPTS[quoteStyle[i]]) {
        optTemp = optTemp | OPTS[quoteStyle[i]]
      }
    }
    quoteStyle = optTemp
  }
  if (quoteStyle & OPTS.ENT_HTML_QUOTE_SINGLE) {
    // PHP doesn't currently escape if more than one 0, but it should:
    string = string.replace(/&#0*39;/g, "'")
    // This would also be useful here, but not a part of PHP:
    // string = string.replace(/&apos;|&#x0*27;/g, "'");
  }
  if (!noquotes) {
    string = string.replace(/&quot;/g, '"')
  }
  // Put this in last place to avoid escape being double-decoded
  string = string.replace(/&amp;/g, '&')

  return string
}