Express.js Insecure Template Engine Usage

High Risk Template Injection
expresstemplatesstijavascripttemplate-enginexss

What it is

The Express.js application uses template engines in an insecure manner, potentially allowing server-side template injection (SSTI), XSS, or other template-related vulnerabilities. This occurs when user input is processed by template engines without proper sanitization or when dangerous template features are enabled.

// Vulnerable: Handlebars without auto-escaping
const handlebars = require('handlebars');

app.engine('hbs', handlebars({
  // Auto-escaping disabled - dangerous
  noEscape: true
}));

app.get('/user', (req, res) => {
  const template = handlebars.compile('<h1>{{name}}</h1>');
  res.send(template({ name: req.query.name })); // XSS risk
});
// Secure: Handlebars with auto-escaping and input validation
const handlebars = require('handlebars');
const validator = require('validator');

app.engine('hbs', handlebars({
  // Auto-escaping enabled by default
}));

app.get('/user', (req, res) => {
  const userName = validator.escape(req.query.name || '');
  const template = handlebars.compile('<h1>{{name}}</h1>');
  
  res.setHeader('Content-Security-Policy', "default-src 'self'");
  res.send(template({ name: userName }));
});

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Express applications use template engines (Pug, EJS, Handlebars, Nunjucks) without enabling automatic HTML escaping, or developers explicitly disable escaping for convenience. Pug templates use unescaped interpolation != instead of escaped = for variable output. EJS templates use <%- variable %> (unescaped) instead of <%= variable %> (escaped). Handlebars uses triple-braces {{{variable}}} allowing raw HTML. Applications configure template engines with {autoescape: false} or similar settings. Without auto-escaping, user-controlled data containing <script> tags, event handlers, or other HTML/JavaScript gets rendered directly into pages, enabling XSS attacks.

Root causes

Template Engines Without Auto-Escaping Enabled

Express applications use template engines (Pug, EJS, Handlebars, Nunjucks) without enabling automatic HTML escaping, or developers explicitly disable escaping for convenience. Pug templates use unescaped interpolation != instead of escaped = for variable output. EJS templates use <%- variable %> (unescaped) instead of <%= variable %> (escaped). Handlebars uses triple-braces {{{variable}}} allowing raw HTML. Applications configure template engines with {autoescape: false} or similar settings. Without auto-escaping, user-controlled data containing <script> tags, event handlers, or other HTML/JavaScript gets rendered directly into pages, enabling XSS attacks.

Passing Unsanitized User Input to Template Variables

Applications pass user input directly from req.query, req.body, req.params, or database queries to template variables without sanitization or validation. Template rendering calls like res.render('profile', {name: req.query.name, bio: req.body.bio}) trust all input as safe. Developers assume template engine escaping provides sufficient protection without understanding escape context requirements (HTML vs JavaScript vs URL). User input containing template syntax, special characters, or malicious payloads gets processed by template engine. Even with some escaping, context-specific attacks (JavaScript context, attribute context, URL context) bypass generic HTML escaping.

Enabling Dangerous Template Features and Filters

Template configurations enable dangerous features like code execution filters, raw output, or server-side includes. Nunjucks templates use |safe filter bypassing escaping: {{userInput|safe}}. Pug allows arbitrary JavaScript with - code lines or in attributes. EJS supports <% arbitrary JavaScript %> code blocks. Applications enable template features designed for trusted template authors but expose them to user-controlled content. Custom template filters or helpers execute arbitrary code when processing user input. Template engines configured to allow dynamic imports, file system access, or module loading create code execution vectors.

Dynamic Template Compilation with User Content

Applications dynamically compile templates using user-provided strings or content. Code uses template.compile(userInput) with Handlebars, pug.compile(req.body.template), or ejs.compile() with user-controlled template source. Applications build templates by concatenating strings including user input: const tmpl = 'Hello ' + userName. Template-as-a-service features allow users to create custom templates, email layouts, or report formats that execute server-side. Even with sandboxing attempts, template compilation with untrusted input creates SSTI vulnerabilities enabling arbitrary code execution through template syntax exploitation.

Missing Content Security Policy for Template Output

Applications render templates without implementing Content Security Policy (CSP) headers that restrict script execution and inline code. No CSP headers means browsers execute any inline scripts, event handlers, or javascript: URLs rendered through templates. Applications don't use nonce-based or hash-based CSP for legitimate inline scripts. Missing CSP allows successful XSS attacks even with partial template escaping because attackers find unescaped contexts. Template rendering doesn't set security headers like X-Content-Type-Options, X-Frame-Options alongside CSP, creating additional attack vectors through MIME sniffing or clickjacking combined with template vulnerabilities.

Fixes

1

Enable Auto-Escaping in Template Engine Configuration

Configure all template engines to enable automatic HTML escaping by default. For Pug, use escaped interpolation = instead of unescaped !=. For EJS, always use <%= variable %> (escaped) and never <%- variable %> (unescaped) with user input. For Handlebars, use {{variable}} (auto-escaped) instead of {{{variable}}} (raw HTML). Configure Nunjucks with {autoescape: true} and avoid |safe filter with user data. Set app.locals.pretty = false in Express to prevent HTML formatting that might break escaping. Test auto-escaping by rendering user input containing <script> tags and verifying they're escaped to &lt;script&gt;. Document in code reviews that unescaped output requires security justification.

2

Sanitize and Validate All User Input Before Templates

Implement input sanitization and validation before passing data to template variables. Use DOMPurify, xss, or sanitize-html libraries to clean user input: const clean = DOMPurify.sanitize(userInput). Validate input against expected patterns using joi, express-validator, or yup before rendering. Remove dangerous characters and HTML tags from user input unless required for functionality. For rich text content, use allowlist-based sanitization that permits only safe HTML tags and attributes. Create sanitization middleware that processes req.body, req.query, req.params before routing to template handlers. Never trust data even from databases without sanitization as it may contain previously injected payloads.

3

Use Template Engines with Secure Defaults

Select template engines designed with security as priority or migrate to more secure alternatives. Use Mustache or Handlebars in strict mode that don't allow code execution. Configure template engines with security options: Nunjucks with {autoescape: true, throwOnUndefined: true}, EJS with {escape: function(str) { return escapeHtml(str); }}. Avoid template engines that allow arbitrary code execution by design (Jade with inline JavaScript, EJS with <% code %>). For new projects, prefer React, Vue, or other component-based frameworks with built-in XSS protection over traditional server-side templating. Review template engine security advisories and upgrade to versions with security fixes promptly.

4

Implement Strict Content Security Policy Headers

Deploy comprehensive Content Security Policy headers using helmet.js CSP middleware to restrict script execution and provide defense-in-depth against template injection. Configure strict CSP: helmet.contentSecurityPolicy({directives: {'default-src': ["'self'"], 'script-src': ["'self'", "'nonce-{random}'"​], 'style-src': ["'self'", "'nonce-{random}'"], 'object-src': ["'none'"], 'base-uri': ["'self'"]}}). Use nonce-based CSP for legitimate inline scripts: generate unique nonce per request, add to CSP header, include in <script nonce="{nonce}"> tags. Set X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block headers alongside CSP. Monitor CSP violation reports to detect XSS attempts.

5

Completely Avoid Dynamic Template Compilation

Never compile templates using user-provided content or strings. Remove all dynamic template compilation code: delete calls to pug.compile(), ejs.compile(), Handlebars.compile() with user input. Pre-compile all templates at application startup or build time. For user-customizable content, use data-driven approaches: store user preferences as data and apply to pre-compiled templates rather than allowing template code modification. If template customization is required, implement strict sandboxing with vm2 module, define allowlist of permitted template constructs, and execute in isolated contexts with no access to require(), process, filesystem. Prefer configuration over code: JSON-based layouts over template strings.

6

Implement Template Sandboxing and Isolation

For template engines requiring dynamic features, implement sandboxing to limit damage from template injection. Use vm2 for isolated JavaScript execution context: const VM = require('vm2'); const vm = new VM({timeout: 1000, sandbox: {data: templateData}}). Configure template engines to run in restricted mode without access to Node.js modules, file system, or process. Implement custom template helpers that validate and sanitize data before rendering. Create allowlists of permitted template functions and filters, rejecting unknown directives. Run template rendering in separate processes with resource limits (CPU, memory, timeout) to contain DoS attacks. Log all template rendering errors and unusual patterns for security monitoring.

Detect This Vulnerability in Your Code

Sourcery automatically identifies express.js insecure template engine usage and many other security issues in your codebase.