JavaScript to JavaScript template language

Generating JavaScript configurtion snippets from templates to be used with the Content-Security-Policy and disabled inline scripts.

Live demo showing strict content security policy banning inlined source yet having dynamic variable injection into separate JavaScript files is at js-to-js.herokuapp.com.

Problem

I have shown the benefits of disabling inline JavaScript in the previous blog post Disable inline JavaScript for security. See these two demos: unprotected and protected to see the common script injection attack and the protection in action.

Unfortunately, the shown method has a big obstacle preventing using it right out of the box.

Most of the variables are injected by the server into the page using inline scripts.

For example, an ExpressJS using Jade template engine could render a page from a view like this

server code
1
2
3
4
5
6
app.get('/', function (req, reqs) {
res.render('index', {
title: 'Example',
analyticsId: '4xy-0123456'
});
});

The Jade page can use the variables title and analyticsIds to set the corresponding values in the HTML text or JavaScript code

index.jade
1
2
3
4
5
6
head
title #{ title }
script.
var analyticsId = '#{ analyticsId }';
script.
// use variable analyticsId to init page analytics

I described a typical example like these in Server-side constants injection into Angular modules, and if we disable the inline scripts, our code will stop working. How do we solve this problem?

Assume we only disable the inlined scripts. Thus our page cannot have any script tags with code in them. We still can have the code in the separate source files. For example we could generate on the fly a JavaScript file with our var analyticsId = ... code and include the reference in the page. It should look something like this

generated analytics-config.js
1
var analyticsId = '4xy-0123456';
analytics.js
1
2
// included after analytics-config.js
// use variable analyticsId to init page analytics
index.jade
1
2
3
4
5
6
head
meta(http-equiv="Content-Security-Policy",
content="script-src 'self';")
title #{ title }
script(src="js/analytics-config.js")
script(src="js/analytics.js")

This approach will work - we do allow non-inlined scripts to run.

How should we generate a JavaScript file dynamically? It is interesting, but searching wide I could not find any template engine whose output would be JavaScript. Even a good (if obsolete) source with lots of template engines did not list any. I really want to have a source JavaScript or JSON file, with default options for simplicity and then substitute run-time values from a middleware or response callback.

This is why I wrote js-to-js - a simple JavaScript to JavaScript template engine middleware, mostly for working with ExpressJS and the like.

JavaScript to JavaScript rendering

It feels a little weird to read JavaScript and render another JavaScript - but this is necessary because we need dynamic server-side rendered scripts. For the example above, we can now do the following steps.

Write a CommonJS module with default configuration and place it in the views folder of the server. For example, we can place the following in views/js/analytics-config.js

1
2
3
module.exports = {
analyticsId: 'default-id'
};

At runtime, the server will transform this file by substituting the actual analytics ID (taken from config or environment, I recommend using nconf for this) and producing another file on the fly that will be returned to the client.

The server is setup like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var express = require('express');
var app = express();
var viewsFolder = join(__dirname, 'views');
app.set('views', viewsFolder);

var jsToJs = require('js-to-js');
app.engine('js', jsToJs);

app.get('/js/analytics-config.js', function (req, res) {
// use this for correct content-type -
// Express will think it is text/html by default
res.setHeader('content-type', 'application/javascript');
// this path is WRT views folder
res.render('js/analytics-config.js', {
// use any run-time values, for example from config
analyticsId: '4xx-xxxxx'
});
});

Our page requests js/analytics-config.js before request actual analytics script

1
2
<script src="js/analytics-config.js"></script>
<script src="js/analytics.js"></script>

and at runtime, the client will receive

1
2
3
4
5
// generated for js/analytics-config.js
var analyticsConfig = {
"analyticsId": "4xx-xxxxx"
};
// source for static "js/analytics.js" follows

Notice that the basename of file js/analytics-config.js was taken ('analytics-config') and then transformed using kebab case to the new variable name inside var analyticsConfig = ... statement;

Fixing other code snippets

A lot of code snippets from 3rd parties are distributed by default using inline script commands, for example, here is the default Google Analytics code snippet given to the website admins to insert into their HTML

1
2
3
4
5
6
7
8
9
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
// 'UA-xxxxxxxx-x' is our unique tracking id
ga('create', 'UA-xxxxxxxx-x', 'auto');
ga('send', 'pageview');
</script>

This script will no longer execute directly from our page! What do we do? Well, first of all, our server should inject the tracking id dynamically. Second, there is nothing magical about this snippet, and nothing prevents us from placing in its entirety into the views script to function as the input. The js-to-js rendering engine will detect if this is a function and will call it with options argument.

We can wrap the above 3rd party snippet in a function expecting an object, but now instead of hard coded tracking id, it will use the property of the options argument.

1
2
3
4
5
6
7
8
9
module.exports = function initGoogleAnalytics(options) {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
// use the provided options.googleAnalyticsId instead of hard coded value
ga('create', options.googleAnalyticsId, 'auto');
ga('send', 'pageview');
};

Then store this snippet in the views/js folder and render it in response to a request, providing a the actual tracking id as a property

1
2
3
4
5
6
7
// express server
app.get('/js/google-analytics.js', function (req, res) {
res.setHeader('content-type', 'application/javascript');
res.render('js/google-analytics.js', {
googleAnalyticsId: 'this-is-a-demo'
});
});

The js-to-js template engine automatically treats functions differently and wraps them in a closure passing the runtime options. Thus the google analytics script will execute something like this when the client requests this page

1
2
3
4
5
6
7
8
9
;((function initGoogleAnalytics(options) {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
// use the provided options.googleAnalyticsId
ga('create', options.googleAnalyticsId, 'auto');
ga('send', 'pageview');
})({ googleAnalyticsId: 'this-is-a-demo' }));

Do not forget to add the https://www.google-analytics.com to the list of allowed script sources in the 'Content-Security-Policy script-src' list.

I like using the options object. It is a lot more manageable in the long run than passing separate arguments. If you need schema validation with the options, take a look at check-more-types.schema method. If you need partial application that works with objects, take a look at obind function.

Conclusions

You can see the live result at js-to-js.herokuapp.com.

demo screenshot

As you can see, the inline script that tried to overwrite the config was disabled, while we could inject dynamic values in two ways. First, we injected an object and changed a property before the second script ran, using the {"analyticsId":"4xx-xxxxx"} object.

Second, the Google Analytics injection worked by wrapping their snippet and executing with an options object that passed a dynamic value for property googleAnalyticsId. This worked fine, because we can see the tracking call to the https://www.google-analytics.com/collect endpoint. If you inspect this GET request, you will see the id we injected being used: id=this-is-a-demo is part of the query.

Using the approach explained in this blog post we can keep unsafe inline JavaScript turned off on our pages, while still relatively easily injecting dynamic configuration into our scripts.

As the last note, remember that this injection is only necessary for JavaScript fragments. You can still inject regular values directly into the page. For example, you can still inject dynamic title using Jade template or the list of allowed CDN hosts

1
2
3
4
head
meta=(http-equiv="Content-Security-Policy",
content="script-src #{ external-cdns } 'self';")
title #{ pageTitle }

Only the script code has to be external to the page.