BorisMoore / jsrender

A lightweight, powerful and highly extensible templating engine. In the browser or on Node.js, with or without jQuery.

Home Page:http://www.jsviews.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Precompile jsrender templates to avoid unsafe-eval

robbertbrak opened this issue · comments

For implementing our Content Security Policy I would like to avoid any code that uses eval() or new Function, so that I don't have to add unsafe-eval to the CSP. However, JsRender uses new Function to compile templates.

Is there a way to avoid this or work around it? Is it on the JsRender roadmap?

Note: I was thinking of solving this by precompiling our templates on the server (something like http://handlebarsjs.com/precompilation.html), so that I only need to render on the client. When I examine the code, it looks like I could make it work by serializing the compiled template (i.e., the result of calling compileTmpl), including all its subtemplates, to a JSON-string on the server, putting that in a <script> tag and then use that to render.

One of the issues I'm running into with this approach, is that the compiled templates also contain a reference to a render function, which is internal to JsRender. I would have to expose it to be able to use it on the client, I think.

I will need to work on adding this as a new feature. I'm not sure yet whether to add it to the V1.0 version that is planned soon, or whether to make it post V1.0. But I have a prototype implementation that you could test, and let me know your thoughts.

Here is the prototype (based on my current working version so has some other changes since V0.9.90):
jsrender_precompilation_candidate.js.txt

Here is how it works.

New API:

var preCompiled = $.views.preCompile(...);

which does pre-compilation, with eval/new Function calls, with same signature options as $.templates(...): markupString / name, markupString / hash of markupStrings for named templates.

It returns an object or a hash of 'precompiled template' objects, which are plain javascript objects, with properties:

  • markup: Markup string,
  • fn: compiled Template function
  • tmplName: Name string
  • template: Hash of 'precompiled template' objects for subtemplates

You can also run it without parameters, in a page that has already got any number of registered templates, and it will return the precompiled objects for that page.

You can run it on the server or in a different time frame, and store the precompiled template objects.

Then at runtime in the browser you can pass exactly the returned object/hash/tree of objects to the normal $.templates method in order to establish the normal template registration, but this will use the pre-compiled functions and will not need to do eval. It will also be faster, and can run on a page with a a lightweight jsrender.runtime.js with no compilation code, if we make that available.

$.templates(preCompiled);

Of course you can hand-optimize the compiled functions, precompiledOb.fn that you pass to $.templates(...).

Usage example:

var data = {name: "Jo", address: {street: "1st Ave.", zip: "120300"}};

$.templates({
	personTmpl: "Name: {{:name}}<br/> {{include address tmpl='addressTmpl'/}}",
	addressTmpl: {
		markup: "Street: {{:street}} {{include tmpl='zipCode'/}}",
		templates: {zipCode: "Zip code: {{:zip}}"}
	}
});

var preCompiled = $.views.preCompile();
//preCompiled.personTmpl.fn = newPersonTmplFn; // Could replace a function by an optimized version
$.templates({personTmpl: null, addressTmpl: null}); // For  test purposes, unregister templates
$.templates(preCompiled); // Register templates using precompiled objects
var html = $.templates.personTmpl(data);

That looks very promising, thanks! Since this is not a breaking API change, it would be great if you could put it in the V1 release. But of course, I leave that decision up to you.

I tried the prototype. It works well with toplevel templates, but it seems to break on subtemplates introduced by {{for}} and {{if}} tags. For example, the following does not render the addresses:

var data = {name: "Jo", addresses: [{street: "1st Ave.", zip: "120300"}, {street: "2nd Ave.", zip: '450600'}]};

$.templates({
  personTmpl: "Name: {{:name}}<br/> {{for addresses}}Street: {{:street}}<br/>{{/for}}"
});

var preCompiled = $.views.preCompile();
//preCompiled.personTmpl.fn = newPersonTmplFn; // Could replace a function by an optimized version
$.templates({personTmpl: null, addressTmpl: null}); // For  test purposes, unregister templates
$.templates(preCompiled); // Register templates using precompiled objects
var html = $.templates.personTmpl(data);

Yes, you are right - the implementation was incomplete. (It was more of a proof of concept).

Here is an update that should work for a range of scenarios: jsviews.js.txt.

It will work with both JsRender and with JsViews. So even in JsViews, all calls to new Function can be avoided by precompiling.

One technique for obtaining the compiled object hierarchy that you need to server-render into the page's javascript (or add statically to the HTML page) is to include {{jsonview/}} in a page to render the complete precompiled object hierarchy:

$.templates("{{jsonview/}}").link("body", $.views.preCompile(tmpl2)); 

You will need this updated jsonview.js, though: jsonview.js.txt

Then you simply select and copy-paste the rendered object hierarchy to your HTML page and pass is to $.templates() to register the templates:

$.templates({...});

Great! That updated version indeed works quite well.

I looks like that modified version of jsonview.js does not output the fn attribute. Based on that idea, though, it was easy to implement a simple serializer that produces workable (copy-pastable) output:

function stringifyJsRender(obj) {
  var str = '{\n';

  ['markup', 'tmplName', 'bnds', '_is'].forEach(function(prop) {
    str += prop + ': ' + JSON.stringify(obj[prop]) + ',\n';
  });

  str += 'fn: ' + obj.fn.toString() + ',\n';
  str += 'tmpls: [';
  str += obj.tmpls.map(stringifyJsRender).join(',\n');
  str += ']\n}';
  return str;
};

function renderPrecompiledTmpl(tmpl) {
  document.body.innerHTML = '';
  document.body.setAttribute('style', 'white-space:pre;overflow:auto;');
  document.body.textContent = stringifyJsRender(tmpl);
};

renderPrecompiledTmpl($.views.preCompile('#my-tmpl'));

Yes, it was skipping all properties of type function (per previous scenarios it was used for).
So here is an updated jsonview.js and jsonview.css which should render functions too.

jsonview.js.txt
jsonview.css.txt

But it will need to be used with the corresponding updated jsviews.js, which is here:
jsviews.js.txt

With this version you can still opt in to the previous behavior by setting {{jsonview ... noFunctions=true .../}}

Hi Robbert, just to let you know that I am working more on this potential feature. Current code is of course not yet working completely (the jsviews.js.txt above) but I will keep you posted here of progress....

I'm happy to hear that. Thank you again for the effort you're putting into this!

Unfortunately, I've decided against moving ahead with this feature. It looked promising, but:

  • I thought it might provide significant performance gains from elimination of running the full template compilation code in the browser - but it turns out that performance in JsRender for template compilation is not so bad (significantly faster than handlebars compilation) - so the gain is not such a big deal,
  • There are many ways in which on-the-fly compilation can occur after the initial compilation of the top-level templates. This is especially so in JsViews, but even in JsRender, you can for example dynamically set the template for a custom tag to some string, in its init() event. If you are running code using that tag, then you will need the full jsrender.js (with compilation), not just the 'runtime' - and new Function will get called the first time you render the tag. You could even change the string for every instance, in non-static ways....

That's too bad, but I understand your decision, now that it turns out to be unfeasible to make it work in the general case. For me personally, however, I think that the special cases in which it does work are sufficient, as I'm only using JsRender and (probably) don't do any of the things that require on-the-fly compilation.

Coming back to my original purpose, which is getting rid of unsafe-eval in our Content Security Policy, is there still some way to get there, even if it means that I cannot use the full strength of JsViews / JsRender? For example: exposing just enough of the compilation API to be able to build my own precompilation?

I'll try to look into alternatives as some point, but probably not until after getting out the next update (which is overdue).... I'll keep this open now, to track that...

Closing for now, but added an After V1? label. I'll consider reopening at some point. (But no promises :) ...)

Hi, is there any plan to get rid of 'unsafe-eval' in our Content Security Policy, Good that we got Eval function case resolved long back but still the 'new Function' is trouble creator.

No plan for the moment. (Although we may re-consider this in the future...). See also the discussion here.

Hi, I know this issue is closed but we need to have JSRender work with precompiled version of templates in a CSP environment with no eval() or new Function(). Adding this to a future release will help our project a lot.

Hello, do you have any plan to add this feature soon? Should we wait or it won't come out?

Unfortunately, following earlier attempts, and associated issues (as discussed earlier in this thread), there is no current plan to provide this feature.