Back to Blog
critical SEVERITY7 min read

How Server-Side Template Injection Happens in EJS and How to Fix It

CVE-2022-29078 is a critical server-side template injection vulnerability in EJS versions prior to 3.1.7 that allows attackers to execute arbitrary code through the `outputFunctionName` parameter. The fix involves upgrading EJS from 2.6.1 to 3.1.7, which implements proper input validation for template rendering options. This vulnerability could allow remote code execution if user-controlled data reaches the template engine without sanitization.

O
By Orbis AppSec
Published August 31, 2026Reviewed August 31, 2026

Answer Summary

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS (Embedded JavaScript templating) versions before 3.1.7, classified as CWE-94 (Improper Control of Generation of Code). The vulnerability exists in the `outputFunctionName` parameter, which can be manipulated to inject arbitrary code into the template rendering pipeline. The fix is to upgrade EJS from 2.6.1 to 3.1.7, which implements stricter validation of template rendering options and prevents injection through template configuration parameters.

Vulnerability at a Glance

cweCWE-94 (Improper Control of Generation of Code)
fixUpgrade EJS from 2.6.1 to 3.1.7 which implements parameter sanitization and validation
riskRemote Code Execution (RCE) if user input reaches template options
languageJavaScript/Node.js
root causeInsufficient validation of outputFunctionName parameter in EJS 2.x, allowing code injection into template rendering context
vulnerabilityServer-Side Template Injection (SSTI) in outputFunctionName parameter

Introduction

In this repository's dependency chain, we discovered a critical server-side template injection vulnerability (CVE-2022-29078) affecting EJS 2.6.1, a widely-used JavaScript templating engine. The vulnerability resides not in how templates are written, but in how template rendering options—specifically the outputFunctionName parameter—are processed. When this parameter receives untrusted input, attackers can inject arbitrary code that executes during template compilation, leading to remote code execution (RCE).

This is particularly dangerous because many developers understand the risks of embedding user data into templates but overlook the risks of embedding user data into template options. The outputFunctionName parameter controls which JavaScript function name EJS generates for template rendering, and in vulnerable versions, this parameter could be manipulated to break out of its intended context and inject malicious code.

The Vulnerability Explained

What Makes This SSTI Unique

Server-side template injection typically involves injecting template syntax (like <%= malicious %>) into template content. However, CVE-2022-29078 is more insidious: it exploits the template configuration layer rather than the content layer.

When EJS 2.6.1 processes templates, the outputFunctionName option is used to customize the name of the compiled rendering function. Here's the problematic pattern:

// Vulnerable code path in EJS 2.6.1
// If outputFunctionName comes from user input:
const options = {
  outputFunctionName: userControlledValue,  // ← Attacker controls this
  filename: templatePath
};

const template = ejs.compile(templateContent, options);

An attacker could supply a value like:

outputFunctionName: "__proto__.constructor.prototype.toString=function(){/*malicious*/}"

Or more directly, inject escape sequences or nested function definitions that break out of the intended rendering context. In EJS 2.x, there was insufficient validation to prevent the outputFunctionName from containing characters or patterns that could alter the generated code structure.

Attack Scenario

Consider a web application that allows users to customize their template rendering settings through a configuration API:

// Vulnerable endpoint (before fix)
app.post('/api/template/render', (req, res) => {
  const userOptions = req.body.templateOptions;  // User-controlled

  const options = {
    outputFunctionName: userOptions.outputFunctionName,  // ← VULNERABLE
    filename: req.body.templatePath
  };

  const result = ejs.render(templateContent, data, options);
  res.json({ result });
});

An attacker sends:

{
  "templateOptions": {
    "outputFunctionName": "x; process.exit(); //"
  },
  "templatePath": "/templates/user.ejs"
}

During template compilation, EJS 2.6.1 would generate code containing this malicious function name without proper escaping, allowing the process.exit() call (or any arbitrary code) to execute when the template is compiled.

Real-World Impact

This vulnerability could allow attackers to:
- Execute arbitrary system commands on the server
- Read sensitive files and environment variables
- Modify application data
- Establish reverse shells for persistent access
- Disrupt service availability

Because template compilation often happens during request processing, this vulnerability creates a direct RCE path from HTTP requests to server-side code execution.

The Fix

The fix involved upgrading EJS from version 2.6.1 to 3.1.7. Here's what changed in the package.json and package-lock.json:

Version Update in package.json

  "lint": {
    "terraform_guidelines.md": "textlint"
+  },
+  "overrides": {
+    "ejs": "3.1.7"
   }
 }

The overrides section ensures that EJS 3.1.7 is pinned, preventing accidental downgrades through transitive dependency resolution.

Dependency Lock Update in package-lock.json

  "node_modules/ejs": {
-   "version": "2.6.1",
-   "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
-   "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
+   "version": "3.1.7",
+   "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz",
+   "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==",
    "license": "Apache-2.0",
+   "dependencies": {
+     "jake": "^10.8.5"
+   },
+   "bin": {
+     "ejs": "bin/cli.js"
+   },

What EJS 3.1.7 Fixed

The EJS 3.1.7 release includes enhanced input validation for the outputFunctionName parameter and other template rendering options. The specific improvements include:

  1. Parameter Sanitization: The outputFunctionName parameter now undergoes strict validation to ensure it only contains valid JavaScript identifier characters (alphanumerics, $, _).

  2. Whitelist-Based Validation: Rather than trying to block malicious patterns (blacklist approach), EJS 3.1.7 uses a whitelist that only allows characters permitted in valid JavaScript identifiers.

  3. Configuration Hardening: Additional template option parameters received similar validation improvements, preventing similar injection vectors through compileDebug, cache, and other options.

The fix ensures that even if outputFunctionName receives attacker-controlled input, it cannot contain escape sequences, semicolons, or other characters that could break out of the intended code generation context.

Why Both Files Changed

  • package.json: The overrides field explicitly pins the safe version, ensuring this fix applies across the entire dependency tree and survives future npm install operations.
  • package-lock.json: Documents the exact resolved version, hash, and new dependencies (like jake) introduced in EJS 3.1.7, ensuring reproducible builds.

Prevention & Best Practices

1. Never Trust Template Configuration Options

Treat template engine configuration parameters (like outputFunctionName, filename, compileDebug) as security-sensitive, just like SQL query parameters:

// ❌ WRONG - Do not do this
const userOptions = req.body.options;
const html = ejs.render(template, data, userOptions);

// ✅ CORRECT - Whitelist and validate
const safeOptions = {
  cache: Boolean(req.body.useCache),  // Explicitly type-coerce
  filename: path.join('/templates', path.basename(req.body.filename))  // Sanitize path
};
const html = ejs.render(template, data, safeOptions);

2. Validate Template Option Types

Enforce strict types for template options at the application level:

const Joi = require('joi');

const optionsSchema = Joi.object({
  outputFunctionName: Joi.string()
    .alphanum()
    .max(50)
    .required(),
  cache: Joi.boolean(),
  filename: Joi.string().required()
});

const { error, value } = optionsSchema.validate(userProvidedOptions);
if (error) throw new Error('Invalid template options');

3. Keep Template Engines Updated

Template injection vulnerabilities are discovered regularly. Use:

# Check for known vulnerabilities
npm audit

# Update to latest secure versions
npm update
npm outdated

4. Use Static Analysis to Detect Unsafe Patterns

Deploy Semgrep rules to catch template option injection at development time:

rules:
  - id: ejs-unsafe-outputFunctionName
    pattern: ejs.render(..., { outputFunctionName: $USER_INPUT, ... })
    message: User input detected in outputFunctionName template option
    severity: ERROR

5. Implement Security Boundaries

Separate template rendering for different trust levels:

// For untrusted templates, use sandbox
const sandbox = {
  data: userProvidedData,
  // Restricted function set
};

// For system templates, use full options
const systemOptions = { cache: true, filename: safeSystemPath };

Key Takeaways

  • Configuration parameters are code: EJS's outputFunctionName parameter directly influences code generation; treating it as data rather than code led to this vulnerability.
  • The outputFunctionName attack vector is specific to template engines: Many Node.js developers understand XSS prevention but may not realize that template engine options can be injection points just like template content.
  • Version 2.6.1 to 3.1.7 represents a major security boundary: EJS 3.x introduced comprehensive parameter validation; using versions prior to 3.1.7 leaves this RCE vector open.
  • Upgrading alone doesn't eliminate user-controlled template options: Even with EJS 3.1.7, passing untrusted input to template rendering options should be avoided; use whitelisting and type validation in your application.
  • The overrides field in package.json is now a security control: This ensures the pinned safe version takes precedence across the entire dependency tree, preventing transitive downgrades.

How Orbis AppSec Detected This

Source: Dependency scanning flagged EJS 2.6.1 in package-lock.json against the CVE-2022-29078 database during static analysis of the project's dependency manifest.

Sink: The vulnerable version is resolved as a transitive or direct dependency; any code in the project calling ejs.render(), ejs.compile(), or accepting template options from external input would be affected.

Missing control: EJS 2.6.1 lacked input validation for the outputFunctionName parameter and other template option fields, allowing untrusted values to influence code generation without sanitization.

CWE: CWE-94 (Improper Control of Generation of Code), CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine)

Fix: Upgraded EJS to version 3.1.7, which implements parameter whitelisting and validation for template rendering options, preventing injection through outputFunctionName and related configuration fields.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2022-29078 demonstrates a subtle but critical class of vulnerabilities: injection attacks that target not template content, but template configuration. While many developers are trained to sanitize data that flows into templates, fewer realize that template engine options can be equally dangerous when controlled by users.

The upgrade from EJS 2.6.1 to 3.1.7 provides immediate protection by implementing strict validation of rendering parameters. However, the deeper lesson is architectural: treat template engine configuration options as security-sensitive input, validate them against strict whitelists, and never allow untrusted users to control how templates are compiled and executed.

By adopting these practices—combined with regular dependency updates and static analysis scanning—you can prevent similar template injection vulnerabilities in your Node.js applications. Template engines are powerful tools, but their power must be wielded carefully when user input is involved.

References

Frequently Asked Questions

What is server-side template injection?

SSTI occurs when user-controlled input is embedded into server-side template rendering without proper sanitization, allowing attackers to inject template directives or code that execute on the server.

How do you prevent SSTI in Node.js template engines?

Never pass user-controlled data directly to template rendering options like `outputFunctionName`, `filename`, or `compileDebug`; always validate and sanitize input, and use allowlists for template configuration parameters.

What CWE covers template injection?

CWE-94 (Improper Control of Generation of Code) and CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine) both cover SSTI vulnerabilities.

Is input validation in template content enough to prevent this SSTI?

No—this vulnerability bypasses content validation entirely because the injection happens through template *configuration parameters* (outputFunctionName), not template content. Configuration-level validation is required.

Can static analysis detect this SSTI?

Yes, static analysis tools can detect when user-controlled variables flow into template engine configuration options like outputFunctionName, especially when combined with dynamic code generation patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1920

Related Articles

high

How Infinite Loop DoS happens in Node.js ID generation and how to fix it

A critical vulnerability in nanoid versions 3.3.16 and below allowed attackers to trigger infinite loops during random ID generation, causing complete CPU exhaustion and denial of service. The fix upgrades to nanoid 3.3.18, which patches the underlying random number generation flaw that could freeze Node.js applications processing untrusted input.

high

How Denial of Service via Invalid UTF-8 Input Happens in Go and How to Fix It

A high-severity Denial of Service vulnerability in golang.org/x/text (CVE-2026-56852) allowed attackers to crash applications by sending malformed UTF-8 input. The fix involved upgrading the dependency from v0.33.0 to v0.39.0, which tightens UTF-8 validation logic and prevents untrusted input from triggering resource exhaustion. This vulnerability demonstrates why timely dependency updates are critical for maintaining application stability and security.

critical

How Prototype Pollution happens in Node.js package managers and how to fix it

A critical prototype pollution vulnerability in loader-utils versions 1.4.0 and 2.0.2 allowed attackers to corrupt JavaScript object prototypes through specially crafted query parameters. The fix upgrades loader-utils to patched versions 1.4.1 and 2.0.4, which sanitize the parseQuery() function's handling of untrusted input and apply stricter dependency constraints.

critical

How missing authorization and code injection happen in Mindustry JavaScript mods and how to fix it

A `TapEvent` handler in `scripts/CommandBlock.js` exposed a full administrative command palette — including a `run-javascript` command that piped player-supplied text straight into `new Function(text)()` — behind nothing more than a team-membership check. Any player who happened to share a team with the block could execute arbitrary JavaScript (and, through Rhino's Java bridge, arbitrary host code) inside the game runtime. The fix adds an explicit `if (!e.player.admin) return;` guard at the top

critical

How URL Injection happens in Node.js template literals and how to fix it

A URL injection vulnerability in `lib/client.js` allowed user-controlled `repo`, `branch`, and `file` parameters to be interpolated directly into fetch URLs without encoding, enabling potential URL manipulation and request hijacking. The fix introduces per-segment percent-encoding via a new `encodePathSegments` helper, neutralizing special characters before they reach the URL construction layer. This closes an exploit primitive that automated attack tooling could chain with other weaknesses.

critical

How a vulnerable websocket-driver dependency happens in Node.js lockfiles and how to fix it

A Trivy scan flagged `websocket-driver@0.7.4` in this repository's `bun.lock` as affected by CVE-2026-54466, a critical issue in a WebSocket protocol handler that parses untrusted HTTP upgrade requests and frame data. The fix upgrades the package to `0.7.5` and adds an explicit `websocket-driver` entry to the lockfile's override block so every transitive consumer — webpack-dev-server, sockjs, faye-websocket — resolves to the patched build instead of the pinned vulnerable one.