Skip to main content

Safe Express.js HTML responses without a templating engine

· 7 min read

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 output syntax {{expression}} is html-escaped, while {{{expression}}} (three curly-braces) is not.

<p>Hello, {{name}}!</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:

Live Editor
Result
Loading...

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 the stringParts

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:

Live Editor
Result
Loading...

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:

Live Editor
Result
Loading...

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

Live Editor
Result
Loading...

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.

tip

There are a few ways to take this a step further by implementing a way to mark strings as "not-escapable". You will likely need that ability, but I'll leave that up to you to decide how to implement

Footnotes
1 Swig has been unmaintained for years. Please don't use it.
2 Content Security Policies (CSP) could protect us against this particular case, but it's just one example to illustrate the point.
3 If you find any bugs or have suggestions, please submit a PR on GitHub