6

Several answers on StackOverflow suggest that a C# object can be passed to JavaScript in ASP.NET MVC by the following:

<script type="text/javascript">
    var obj = @Html.Raw(Json.Encode(Model));
</script>

This is vulnerable to XSS. If any variable in the model contains </script>, this isn't encoded by JSON.Encode, and HTML encoding is turned off because of Html.Raw.

However, if we remove the @Html.Raw, the JSON is HTML-encoded, and not correctly decoded. If our model contains 1 < 2, this is encoded into 1 &lt; 2, and JavaScript sees 1 &lt; 2. Should it we encoded like 1 \x3c 2? That works for strings.

How do I correctly pass a JSON-encoded object to JavaScript, where the < stay intact, but it is also not vulnerable to XSS?

Sjoerd
  • 28,707
  • 12
  • 74
  • 102
  • If a string variable in your model contains `` then this should be ignored by the html parser as it is contained within a JS string and Json.Encode makes sure it really stays within a string. – Robert Aug 27 '21 at 02:07

1 Answers1

5

Use an HTML data-* attribute to store the object as JSON and later parse it with JS. This has several security and design advantages over injecting directly into an inline <script>.

Example

Let's inject the JSON-serialized object into the data-obj attribute of a <div>:

<div id="appdata" data-obj="@Json.Encode(Model)"></div>

Now read the JSON string through the .dataset property and deserialize it:

<script type="text/javascript">
    var obj = JSON.parse(document.getElementById('appdata').dataset.obj);
</script>

If you're using jQuery, you can be even more concise since the JSON string gets implicitly JSON.parse()'d:

var obj = $('#appdata').data('obj');

Rationale

The advantage of this approach is that it doesn't introduce any advanced, nested injection contexts.

The template engine just has to prepare a value for an ordinary HTML attribute here. No need to be aware of a special JS context and its syntax idiosyncracies. This pattern also side-steps other common issues with injecting user data into JS, like syntax-breaking newlines or invalid escape sequences.

Not having to use Html.Raw() also facilitates code analysis. E.g., you don't get the noise of a static vulnerability checker that flags the expression as potentially dangerous but is unable to reason through the JS context to confirm whether it's a true positive – forcing you to manually review the filter logic.

By keeping contexts separate and not dynamically generating JS code, you also preserve the option of loading all JS from remote static sources with all the benefits of minimizing, caching, etc. and possibly setting up a strong CSP without 'unsafe-inline' directives.


The data-* attribute approach is essentially also recommended in the docs, see: Prevent Cross-Site Scripting (XSS) in ASP.NET Core

Arminius
  • 43,922
  • 13
  • 140
  • 136