There are too many choices for server-side rendering HTML pages. Avoiding the heavy hitter frameworks like React, that require a lot more work and will drain server resources quickly, you may find yourself reaching for a templating engine that can dynamically help you build up HTML responses for your JavaScript server.
Having written a templating engine1, I know that many of them include features that make them really attractive to many users. However, you may really not need one at all.
The biggest feature that templating engines tend to provide is safety from Cross-Site Scripting (XSS). Let’s look at a very simplistic example. Let’s say you want to greet a person by name by using some user-submitted information. To make it easy, let’s grab that value from the query param ?name
from the URL. Fire this snippet up with Node.js:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send(`<p>Hello, ${req.query.name}!</p>`);
});
app.listen(3000);
When I run the above and visit http://localhost:3000/?name=Paul
, I am greeted with Hello, Paul!
. Awesome! But there’s just one problem. What if someone shares a link that looks like this: http://localhost:3000/?name=%3Cscript%3Ealert(%27got%20you%27);%3C/script%3E
?
You might see the problem without even pasting that in your browser. The server will directly inject a <script>
and allow the “attacker” to execute any JavaScript on the page. While this example doesn’t do anything bad other than alert on the page, more complicated attacks could load scripts from somewhere else2. This kind of attack is known as Cross Site Scripting (or XSS for short).
Templating engines
So, with XSS in mind, many templating engines have, by default, protected against that by making their default variable insertion escape the output unless the developer marks it as safe.
Handlebars
Handlebars output syntax {{expression}}
is html-escaped, while {{{expression}}}
(three curly-braces) is not.
<p>Hello, {{name}}!</p>
EJS
EJS ouput syntax <%=
is html-escaped, while <%-
is not.
<p>Hello, <%= name %>!</p>
Swig
Swig ouput syntax {{ variable }}
is html-escaped, while {{ variable|raw }}
is not.
<p>Hello, {{variable}}!</p>
Marko
Marko ouput syntax ${variable}
is html-escaped, while $!{variable}
is not.
<p>Hello, ${escaped}!</p>
Of all of the above examples, we notice that automatic HTML-escaping is the default and recommended syntax for each language. And that’s really awesome! But again, you might be able to do this much quicker and simpler.
Moving away from template engines
Originally, at Twitter, we had written part of our server-side HTML rendering using Handlebars. At the time (2014/15), it was the best choice. It had all of the general features you’d want: automatic HTML escaping was a big piece of that.
We had also looked into JavaScript tagged templates. However, we weren’t quite transitioned to Node 4.0.0 (released 2015-09-08) and tagged templates weren’t available to us.
Fast-forward 5 years and I had been auditing a bunch of our server rendering code. What really stuck out to me was that we were using Handlebars and it was largely unchanged. We didn’t use any of the more complex features and the indirection was causing a few issues with figuring out where code was between the logic of our middleware and the actual rendering of the HTML itself.
Tagged template functions are really cool and they’re pretty simple themselves, just with a funny syntax (that you may recognize from implementations of things like Styled Components).
Let’s implement a simple tagged template function first, to get the feel for it. The following is a live editor, so play around with the variable that we insert:
function example() {
function html(stringParts, name) {
return `${stringParts[0]}${name}${stringParts[1]}`;
}
// Change this value to something that might be part of an XSS attack
// eg: <script>alert("hello");</script>
const name = 'Paul';
return html`Hello, ${name}!`;
}
In the above, you can see the two parts that make up a tagged template function:
stringParts
is an array of strings. Think of these as each part of the tagged string split up by each variable${...}
. If you don’t have any variables, the length will be 1. For 1 variable, the length will be two; 2 variables will be length 3, and so on.name
(and any further arguments) are the variables that you are inserting into thestringParts
This works well, but we’d like it to accept any number of inserted variables. To do that, we can spread the arguments and write a reduce the stringParts, merging them with each value in order:
function example() {
function html(stringParts, ...values) {
return stringParts.reduce((accumulator, part, i) => {
const value = values[i] || '';
return `${accumulator}${part}${value}`;
}, '');
}
const personOne = 'Jane';
const personTwo = 'Jimmy';
return html`Hello, ${personOne} and ${personTwo}!`;
}
Strings only
We still haven’t fixed our big security hole, though! Starting with the most basic, we can escape each value using the escape-html package when we add it to the accumulator. This time, you can see that attempting to write a <script>
tag to the output ends up getting escaped, exactly as we would like:
// Commented out to work with the blog's live editor
// import escape from 'escape-html';
function example() {
function html(stringParts, ...values) {
return stringParts.reduce((accumulator, part, i) => {
const value = values[i] || '';
return `${accumulator}${part}${escape(value)}`;
}, '');
}
const name = '<script>alert("hello");</script>';
return html`Hello, ${name}!`;
}
A fully-featured result
This works really well, is simple and fast. For completeness, we can take it a couple of steps further and ensure that any value
can be mixed
type (JSON, Date, etc):
// Commented out to work with the blog's live editor
// import escape from 'escape-html';
function example() {
function html(stringParts, ...values) {
return stringParts.reduce((accumulator, part, i) => {
const value = values[i] || '';
let escapedValue = '';
// If we have a string, just escape it
if (typeof value === 'string') {
escapedValue = escape(value);
// Arrays and plain JavaScript objects can be JSON stringified with a custom function to ensure each individual value gets escaped
} else if (value && (Array.isArray(value) || Object.prototype.toString.call(value) === '[object Object]')) {
escapedValue = JSON.stringify(value, (key, value) => (typeof value === 'string' ? escape(value) : value));
// If we're dealing with a class of some other type, try to use the `toString` method
} else if ('toString' in Reflect.ownKeys(value)) {
escapedValue = escape(value.toString());
// otherwise, just escape the String value
} else {
escapedValue = escape(String(value));
}
return `${accumulator}${part}${escapedValue}`;
}, '');
}
const name = 'Paul';
const jsonContent = { content: '<script>alert("hello");</script>' };
return html`<h1>Hello, ${name}!</h1>
<script>
window.DATA = ${jsonContent};
</script> `;
}
And that’s it! That’s the full implementation3 of a Tagged Template function for JavaScript-based servers that allows you to output XSS-safe. Any HTML within the stringParts
content will be written verbatim, while all variables injected will automatically be escaped.