Secure way of inserting dynamic values in external

2019-01-15 13:28发布

问题:

I'm implementing Content Security Policy headers using the following policy

Content-Security-Policy: default-src 'self'

so will need to avoid inline script because it will not execute.

However, in the MVC application certain functionality such as editor templates use inline script. e.g. tinymce_jquery_full.cshtml contains

$(function() { 

    $('#@ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)').tinymce({
...

What is a good way to include dynamic values in external .js files when using a CSP?

My current thinking is one of two ways:

C# Generated JavaScript

Similar to the way JSONP works, except that in the URL I do not specify a call back - I simply pass the dynamic values. In each file that needs dynamic JavaScript I include a link such as

<script src="/script/Foo/bar"></script>

which hits ScriptController's Foo action which returns content of type text/javascript inserting the dynamic value bar. This strikes me as a reliable way, if a bit clunky and it kind of defeats some of the advantages of using a CSP in the first place (almost as easy to accidentally insert unencoded text and cause XSS as it is without a CSP).

Hidden Form Fields

Dynamic values are inserted into the page:

<input type="hidden" id="url" name="url" value"http:://www.example.com/" />

and these values are looked up in the external JavaScript files:

$('#url').val();

This will work, but it could be awkward if there are several dynamic controls on the page or if there are more than one control of the same type. The problem is how to efficiently match each .js script to its hidden field(s).

Is there any better solution for this or are there any ready made frameworks I can make use of?

回答1:

$('#@ViewData.TemplateInfo.GetFullHtmlFieldName(string.Empty)')

Yeah this isn't a good approach in general. Razor will HTML-escape by default but the context isn't simply HTML here, it's:

  • an identifier, inside
  • a CSS selector, inside
  • a JavaScript string literal, inside
  • a JavaScript statement, inside
  • an HTML CDATA element (<script>)

so just HTML-escaping is the wrong thing - any characters that are special in the context of selectors or JS string literals (such as dot, backslash, apostrophe, line separators...) would break this statement.

Maybe that's not super-important for this specific example, assuming the result of GetFullHtmlFieldName is always going to be something safe, but that's not a good assumption for the general case.

C# Generated JavaScript: a bit clunky and it kind of defeats some of the advantages of using a CSP

Agreed, avoid this. Getting the caching right and transferring the information from the ASP that generates the page content to the page that generates the script is typically a pain.

Also generating JavaScript is not necessarily that simple. ASP and Razor templates don't give you an automatic JS escaper. JSON encoding nearly does it, except for the niggling differences between JSON and JS (ie U+2028 and U+2029).

Hidden Form Fields

More generally, putting the information in the DOM. Hidden form fields are one way to do this; data- attributes are another commonly-used one. Either way I strongly prefer the DOM approach.

When using hidden form fields you should probably remove the name attribute as if they're inside a real <form> you typically don't actually want to submit the data you were passing into JS.

This way you can inject the content into the template using normal default-on HTML escaping. If you need more structure you can always JSON-encode the data to be passed. jQuery's data() helper will automatically JSON.parse them for you too in that case.

<body data-mcefields="@Json.Encode(GetListOfHtmlFields())">
...
var mce_array = $('body').data('mcefields');
$.each(mce_array, function() {
    var element = document.getElementById(this);
    $(element).tinymce({ ... });
});

(Although... for the specific case of selecting elements to apply an HTML editor to, perhaps the simpler way would be just to use class="tinymce" and apply with $('.tinymce').tinymce(...)?)