Secure Node.js Templating with EJS and node:tpl

Using templating engines in Node.js simplifies dynamic HTML generation, but it introduces significant security risks like Cross-Site Scripting (XSS) and Remote Code Execution (RCE) if handled improperly. This article explores how to safely utilize traditional templating engines like EJS alongside Node’s native templating capabilities. You will learn actionable best practices, including input sanitization, strict escaping, and secure configuration tweaks, to keep your Node.js applications safe from injection vulnerabilities.


Securing EJS (Embedded JavaScript)

EJS is a popular and flexible templating engine, but its flexibility makes it prone to XSS attacks if developers use the wrong syntax.

1. Always Use Escaped Output

EJS provides two primary ways to output data: * <%= value %>: Escapes HTML characters (Safe). * <%- value %>: Renders raw HTML (Unsafe).

Always default to <%= %> when rendering user-supplied data. Only use <%- %> when you are absolutely certain the source of the HTML is trusted and pre-sanitized.

// SAFE: HTML characters are escaped automatically
app.get('/profile', (req, res) => {
    res.render('profile', { username: req.query.name }); 
});

2. Sanitize Raw HTML Input

If your application must render raw HTML (using <%- %>), sanitize the data before passing it to the EJS template. Use a robust sanitization library like sanitize-html to strip out dangerous elements like <script> tags, inline event handlers, and javascript: URIs.

const sanitizeHtml = require('sanitize-html');

app.post('/comment', (req, res) => {
    const cleanComment = sanitizeHtml(req.body.comment, {
        allowedTags: [ 'b', 'i', 'em', 'strong', 'a' ],
        allowedAttributes: {
            'a': [ 'href' ]
        }
    });
    res.render('blog', { comment: cleanComment });
});

3. Avoid Dynamic Template Compilation

Never compile EJS templates from untrusted user inputs. Functions like ejs.compile(untrustedString) or passing untrusted variables into ejs.render() can lead to Server-Side Template Injection (SSTI), allowing attackers to execute arbitrary system commands on your server.


Secure Native Templating in Node.js

While Node.js does not have a heavy, built-in “node:tpl” standard library file parser in stable releases, developers frequently use native ES6 Tagged Template Literals for native templating without external dependencies.

Utilizing Tagged Templates for Auto-Escaping

You can write a simple tagged template function that automatically sanitizes and escapes interpolated values, rendering native string templates safely.

function safeHtml(strings, ...values) {
    return strings.reduce((result, string, i) => {
        const value = values[i - 1];
        // Escape HTML special characters
        const escapedValue = String(value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
        return result + escapedValue + string;
    });
}

const userInput = '<script>alert("hack")</script>';
// SAFE: The output will be safely escaped
const secureOutput = safeHtml`<div>User input is: ${userInput}</div>`;

General Security Best Practices

Regardless of whether you use EJS, custom native template literals, or other engines like Pug or Handlebars, apply these universal defense-in-depth strategies:

  1. Implement a Content Security Policy (CSP): Configure robust CSP headers using the helmet middleware. A strong CSP prevents unauthorized scripts from executing, even if an attacker successfully injects a malicious payload into your template.
  2. Context-Aware Encoding: Remember that HTML escaping does not protect you if you insert user input directly inside <script> blocks, CSS style tags, or tag attributes (e.g., <a href="USER_INPUT">). Ensure data passed to these contexts is sanitized specifically for that environment.
  3. Strict Content Type Headers: Ensure your application explicitly sends Content-Type: text/html; charset=utf-8 to prevent browsers from misinterpreting raw text outputs as executable scripts.