5

I know of several ways to do this, but they all have some downside. Is there an "accepted" way of doing it, that is considered the best?

I used to use the Microsoft.Security.Application.AntiXss.JavaScriptEncode() which is great, but AntiXSS has been end-of-lifed because the encoder is now included in .NET as of 4.5.

However, for some reason, System.Web.Security.AntiXss.AntiXssEncoder doesn't include the JavaScriptEncode method.

There's System.Web.HttpUtility.JavaScriptStringEncode(), but it uses a blacklist method for encoding, so it's unlikely to be as good as a whitelist encoder.

And I've seen some recommendations to use System.Web.Script.Serialization.JavaScriptSerializer.Serialize(), but that just calls HttpUtility.JavaScriptStringEncode().

So what's currently the best accepted, whitelist method for encoding values written out as JS variables?

Elezar
  • 1,467
  • 1
  • 15
  • 22

3 Answers3

7

If you wish to include JavaScript code within a <script> block like this:

<script>
var myVariable = '<%=thisIsWrong %>';
</script>

Then in this context HttpUtility.JavaScriptStringEncode should be used. This function also correctly encodes special characters, so if </script> was to be rendered in a script tag in an attempt to close the HTML script tag ready for an XSS attack, it would be rendered as:

\u003c/script\u003e

which is the correct encoding for JavaScript to understand it as </script>, but without the browser interpreting it as a literal closing script tag. Some naively written JavaScript encoding routines would not convert this because the sequence does not contain \, " or ' characters.

If you don't make sure that closing script tags are not rendered, then an attack like so is possible. Imagine this is the input to your application:

</script><script>alert(1)</script>

which the renders in the browser as

<script type="text/javascript">

alert('</script><script>alert(1)</script>');

</script>

and the browser will interpret the script tag ending at alert('</script> and simply execute what is in the new script tag.

With the JavaScriptStringEncode function this is safe as it is rendered as:

<script type="text/javascript">

alert('\u003c/script\u003e\u003cscript\u003ealert(1)\u003c/script\u003e');

</script>

which does not contain </script> for the browser to interpret.

There's System.Web.HttpUtility.JavaScriptStringEncode(), but it uses a blacklist method for encoding, so it's unlikely to be as good as a whitelist encoder.

Some of the other encoding functions in .NET do use blacklist methods, however in my own testing JavaScriptStringEncode seems to be ample.

The OWASP recommendation for JavaScript is

Except for alphanumeric characters, escape all characters less than 256 with the \xHH format to prevent switching out of the data value into the script context or into another attribute.

so you could easily write your own to comply with this.

Note that if you want to include code in attribute tags:

<a href="http://example.com" onclick="alert('<%=wrong>')">Click</a>

then the OWASP method means you don't have to take care of HTML encoding too (because no HTML characters with special meaning are actually output). Without (e.g. with JavaScriptScriptEncode) you need to HTML encode too.

Having said all this, a safer way is to approach it like the answer to my question Secure way of inserting dynamic values in external JavaScript files. Use data- attributes to place the dynamic values in the DOM (in HTML) and then use JavaScript to extract these values. This will prevent all JavaScript encoding headaches.

Community
  • 1
  • 1
SilverlightFox
  • 32,436
  • 11
  • 76
  • 145
  • The AntiXss source that Kevin Hakanson posted does handle encoding the entire . It outputs `alert('\x3c\x2fscript\x3e\x3cscript\x3ealert\x281\x29\x3c\x2fscript\x3e');` rather than using unicode escape codes, but as you pasted, that is acceptable per OWASP. And yes, I'm sure that `System.Web.HttpUtility.JavaScriptStringEncode()` uses a blacklist. It's a very straightforward can be easily checked by decompiling it. I do think this is the best answer though, for pointing out how simple the OWASP recommendation is. I think this is a case where rolling my own is perfectly acceptable. – Elezar Jun 09 '15 at 04:08
3

If you don't put an unsafe string into a place where it can be interpreted as script (like between <script> tags), you never have to worry about XSS. I know not everything can be redesigned easily, but if I were you, I'd try to either put the data (HTML Encoded) into a hidden HTML element and read the value of that element in JavaScript, or do an AJAX request for the variable in JSON format.

Kevin
  • 5,874
  • 3
  • 28
  • 35
  • Yeah, that's what we do with newer code. However, we have a lot of older code that doesn't work this way but does call the AntiXSS `JavaScriptEncode` method. Changing that to call some other encoding method would be fairly simple. But trying to change all those places to write out to attributes or properties on HTML elements, and change the JS code that uses what is currently output to read from those HTML elements, would be a huge amount of work. – Elezar Jun 09 '15 at 03:53
  • I hear ya. Didn't want to come off as one of those folks whose every answer to a "how" question is "Don't. Do it a different way." but wanted to mention in case you hadn't considered. – Kevin Jun 09 '15 at 16:51
  • An unsafe string can *include* – user2864740 Aug 23 '16 at 21:35
  • @user2864740 sorry if "HTML encoded" wasn't implied... I thought it went without saying these days. With Razor, you have to go out of your way to get a variable sent down in raw text. I updated the answer accordingly, and I'm sure that a lot of the newer devs who will Google this in the future would benefit from knowing that the most direct answer to their immediate problem is not necessarily the best approach. It's better to treat data only as data when possible. – Kevin Aug 23 '16 at 22:24
2

The source code for Encoder.cs is available and "subject to the Microsoft Permissive License"

    public static string JavaScriptEncode(string input, bool emitQuotes)
    {
        // Input validation: empty or null string condition
        if (string.IsNullOrEmpty(input))
        {
            return emitQuotes ? JavaScriptEmptyString : string.Empty;
        }

        // Use a new char array.
        int outputLength = 0;
        int inputLength = input.Length;
        char[] returnMe = new char[inputLength * 8]; // worst case length scenario

        // First step is to start the encoding with an apostrophe if flag is true.
        if (emitQuotes)
        {
            returnMe[outputLength++] = '\'';
        }

        for (int i = 0; i < inputLength; i++)
        {
            int currentCharacterAsInteger = input[i];
            char currentCharacter = input[i];
            if (SafeListCodes[currentCharacterAsInteger] != null || currentCharacterAsInteger == 92 || (currentCharacterAsInteger >= 123 && currentCharacterAsInteger <= 127))
            {
                // character needs to be encoded
                if (currentCharacterAsInteger >= 127)
                {
                    returnMe[outputLength++] = '\\';
                    returnMe[outputLength++] = 'u';
                    string hex = ((int)currentCharacter).ToString("x", CultureInfo.InvariantCulture).PadLeft(4, '0');
                    returnMe[outputLength++] = hex[0];
                    returnMe[outputLength++] = hex[1];
                    returnMe[outputLength++] = hex[2];
                    returnMe[outputLength++] = hex[3];
                }
                else
                {
                    returnMe[outputLength++] = '\\';
                    returnMe[outputLength++] = 'x';
                    string hex = ((int)currentCharacter).ToString("x", CultureInfo.InvariantCulture).PadLeft(2, '0');
                    returnMe[outputLength++] = hex[0];
                    returnMe[outputLength++] = hex[1];
                }
            }
            else
            {
                // character does not need encoding
                returnMe[outputLength++] = input[i];
            }
        }

        // Last step is to end the encoding with an apostrophe if flag is true.
        if (emitQuotes)
        {
            returnMe[outputLength++] = '\'';
        }

        return new string(returnMe, 0, outputLength);
    }
Kevin Hakanson
  • 41,386
  • 23
  • 126
  • 155
  • 1
    This is good to know, however the only reason I would need the source is if I wanted to update it or recompile it against new versions of the framework. And once I start doing that, then I'm essentially violating the best-practice of "use widely-available, vetted libraries for security, rather than trying to roll your own." – Elezar Jun 09 '15 at 03:49