Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Precompile jsrender templates to avoid unsafe-eval #336

Closed
robbertbrak opened this issue Apr 12, 2018 · 16 comments
Closed

Precompile jsrender templates to avoid unsafe-eval #336

robbertbrak opened this issue Apr 12, 2018 · 16 comments

Comments

@robbertbrak
Copy link

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.

@BorisMoore
Copy link
Owner

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);

@robbertbrak
Copy link
Author

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);

@BorisMoore
Copy link
Owner

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({...});

@robbertbrak
Copy link
Author

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'));

@BorisMoore
Copy link
Owner

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 .../}}

@BorisMoore
Copy link
Owner

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....

@robbertbrak
Copy link
Author

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

@BorisMoore
Copy link
Owner

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....

@robbertbrak
Copy link
Author

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?

@BorisMoore
Copy link
Owner

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...

@BorisMoore
Copy link
Owner

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

@dasarghya33
Copy link

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.

@BorisMoore
Copy link
Owner

BorisMoore commented May 28, 2022

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

@build3dparts
Copy link

build3dparts commented Nov 23, 2022

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.

@YannickLafont
Copy link

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

@BorisMoore
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants