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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
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:
- Implement a Content Security Policy (CSP):
Configure robust CSP headers using the
helmetmiddleware. A strong CSP prevents unauthorized scripts from executing, even if an attacker successfully injects a malicious payload into your template. - 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. - Strict Content Type Headers: Ensure your
application explicitly sends
Content-Type: text/html; charset=utf-8to prevent browsers from misinterpreting raw text outputs as executable scripts.