Insights

How to Use Handlebars.js When Unsafe-Eval is Disabled

Navigating content security policies for Handlebars.js

Understanding Handlebars.js and Content Security Policies

If you are changing your website’s Content Security Policy(CSP) to disable unsafe-eval, or if unsafe-eval is already disabled, you might notice the following error in your Console when trying to use Handlebars.js

EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self’

This is because Handlebars uses Function() which is gated by the CSP, and we can solve this error by using precompiled templates.

Setting Up a Basic Handlebars Demo

For this blog, we’re just going to set up a simple Handlebars demo. You can install handlebars by including the js file from CDN

<script src="https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.min-v4.7.8.js"></script>

Note that the file has the compiler + runtime. The compiler has the Function() which won’t work with the CSP so later on, we will be switching to just the runtime file.

Make a file called index.html, put the following code

<div class="handlebar-demo"></div>

<script id="handlebars-demo" type="text/x-handlebars-template">
    <div>
       My name is {{name}}. I am a {{occupation}}.    
    </div>
</script>

<script src="index.js"></script>

Create an index.js file and paste the following code

function handlebarTest() {
    var template = $('#handlebars-demo').html();

    var context = { "name": "Gorman Law", "occupation": "developer" };

    //Compile the template data into a function
    var templateScript = Handlebars.compile(template);

    var html = templateScript(context);

    $('.handlebar-demo').html(html);
}

handlebarTest();

If done correctly, you should see the following

Example text introducing a developer named Gorman Law

Adding a CSP

In the <head> of your html, add the following code

   <meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' *; script-src 'self'"> -->

This should result in the following errors in your console.

Error message showing eval is not allowed due to content security policy

Remember this code from above? var templateScript = Handlebars.compile(template); we are going to change it to precompile the template.

Precompiling Handlebars Templates

The Handlebars website says it’s more efficient to compile templates beforehand. You can include the smaller runtime, and the browser will be faster because it doesn’t have to compile them on the fly. A win-win all around.

  1. Install Handlebars npm in your working directory npm install handlebars

  2. Create a new file in the same directory called demo.hbs . Cut the <script> from your index.html file and paste it into your new file. Remove the <script> tags

<div>
    My name is {{name}}. I am a {{occupation}}.    
</div>
  1. Open up Terminal, and navigate to your working directory, then run the following command

    handlebars demo.hbs -f demo.precompiled.js

    A new file called demo.precompiled.js should appear in your directory, and if you open it up, it should contain the following code.

(function() {
  var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['demo'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
        if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
          return parent[propertyName];
        }
        return undefined
    };

  return "<div>\r\n    My name is "
    + alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":23}}}) : helper)))
    + ". I am a "
    + alias4(((helper = (helper = lookupProperty(helpers,"occupation") || (depth0 != null ? lookupProperty(depth0,"occupation") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"occupation","hash":{},"data":data,"loc":{"start":{"line":2,"column":32},"end":{"line":2,"column":46}}}) : helper)))
    + ".    \r\n</div>";
},"useData":true});
})();
  1. Include the precompiled js file in index.html file

    <script src="demo.precompiled.js"></script>

  2. Change the handlebarTest function inside index.js file

function handlebarTest() {

    var context = { "name": "Gorman Law", "occupation": "developer" };

        //template no longer needs to be compiled!
        // Notice the templates['demo'] in the precompiled.js file. You can access it
        // with Handlebars.templates.demo or Handlebars.templates['demo']
    var template = Handlebars.templates.demo;

    var html = template(context);

    $('.handlebar-demo').html(html);
}
  1. Change the Handlebars source to the runtime version <script src="[https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.min-v4.7.8.js](https://s3.amazonaws.com/builds.handlebarsjs.com/handlebars.runtime.min-v4.7.8.js)"></script>

After following all these steps, you should be able to see your Handlebars rendering appear on your page again, and no more errors in the console!

Using Gulp

If you have a lot of templates, manually compiling each one can be time consuming, and make the project bloated with so many precompiled JS files. Gulp can compile all the templates into one file for you.

First, install the dependencies

npm install --save-dev gulp-handlebars gulp-wrap gulp-declare gulp-concat

gulp-wrap and gulp-declare are going to be used to declare template namespaces and make templates available for use in the browser

Let’s pretend this is our working directory structure

├── gulpfile.js              // gulpfile
├── index.html               // html file
├── index.js                 // js file
└── templates/               // folder with all our hbs files for precompilation
    └── demo.hbs             // demo handlebar template from above
└── precompiled/             // folder for our precompiled template js
      └── templates.js         // our gulp output file

Enter the following in your gulpfile

var gulp = require('gulp');
var handlebars = require('gulp-handlebars');
var wrap = require('gulp-wrap');
var declare = require('gulp-declare');
var concat = require('gulp-concat');

gulp.task('templates', function() {
    return gulp
      .src('templates/*.hbs')        //will compile every file with .hbs extension
    .pipe(handlebars())
    .pipe(wrap('Handlebars.template(<%= contents %>)'))
    .pipe(declare({
      namespace: 'Example.templates',
      noRedeclare: true, // Avoid duplicate declarations
    }))
    .pipe(concat('templates.js'))
    .pipe(gulp.dest('precompiled/'));
});

gulp.task('default', gulp.series('templates'));

If you run your gulpfile, you should notice that templates.js is now in your precompiled folder! Opening it up you should see this:

this["Example"] = this["Example"] || {};
this["Example"]["templates"] = this["Example"]["templates"] || {};
this["Example"]["templates"]["demo"] = Handlebars.template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
        if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
          return parent[propertyName];
        }
        return undefined
    };

  return "<div>\r\n    My name is "
    + alias4(((helper = (helper = lookupProperty(helpers,"name") || (depth0 != null ? lookupProperty(depth0,"name") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data,"loc":{"start":{"line":2,"column":15},"end":{"line":2,"column":23}}}) : helper)))
    + ". I am a "
    + alias4(((helper = (helper = lookupProperty(helpers,"occupation") || (depth0 != null ? lookupProperty(depth0,"occupation") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"occupation","hash":{},"data":data,"loc":{"start":{"line":2,"column":32},"end":{"line":2,"column":46}}}) : helper)))
    + ".    \r\n</div>";
},"useData":true});

Here’s what we need to change to make your page work again

  1. Inside index.html, change the .js include from

    <script src="demo.precompiled.js"></script>

    to

    <script src="precompiled/templates.js"></script>

  1. Inside your index.js file, change the function again to this
function handlebarTest() {

    var context = { "name": "Gorman Law", "occupation": "developer" };

        // Notice how the templates['demo'] changed to this["Example"]["templates"]["demo"]. 
        // Here's how we can use the new namespace.
    var template = Example.templates.demo;

    var html = template(context);

    $('.handlebar-demo').html(html);
}

You should now see the page re-appear again!

Achieving Compatibility With Handlebars and CSP

To get around unsafe-eval for scripts, we have to use precompiled-templates for Handlebars.js. I hope this blog post helped you get your templates up and running fast!



Meet Gorman Law

Full Stack Developer

🏋️🎸🏈

Gorman is a Full Stack Developer and a University of Calgary alumni who has a background in Canada's financial industry. Outside of work, he likes to go rock climbing, try out new ice cream places, and watch sports.

Connect with Gorman