Back to Blog
critical SEVERITY6 min read

How server-side template injection happens in Node.js EJS and how to fix it

A critical server-side template injection vulnerability (CVE-2022-29078) was discovered in EJS version 3.1.6, allowing attackers to execute arbitrary code on the server through the `outputFunctionName` option. This vulnerability was fixed by upgrading EJS from 3.1.6 to 3.1.7 in the react-redux-bad-algo project, eliminating a dangerous remote code execution vector.

O
By Orbis AppSec
Published July 30, 2026Reviewed July 30, 2026

Answer Summary

CVE-2022-29078 is a critical server-side template injection (SSTI) vulnerability in EJS (Embedded JavaScript) for Node.js, classified as CWE-94 (Improper Control of Generation of Code). Attackers can exploit the `outputFunctionName` render option to inject and execute arbitrary JavaScript on the server. The fix is straightforward: upgrade EJS from version 3.1.6 to 3.1.7 or later, which adds proper validation to prevent code injection through template options.

Vulnerability at a Glance

cweCWE-94 (Improper Control of Generation of Code)
fixUpgrade EJS from 3.1.6 to 3.1.7
riskRemote code execution allowing full server compromise
languageJavaScript (Node.js)
root causeUnsanitized `outputFunctionName` option allowed arbitrary code injection during template compilation
vulnerabilityServer-Side Template Injection (SSTI)

Introduction

In the react-redux-bad-algo project, a critical server-side template injection vulnerability was lurking in the dependency tree. The package-lock.json file pinned EJS (Embedded JavaScript) to version 3.1.6, which contains CVE-2022-29078—a flaw that could allow attackers to execute arbitrary code on the server by manipulating the outputFunctionName render option.

This isn't a theoretical risk. The vulnerability is rated critical with a CVSS score of 9.8, and proof-of-concept exploits are publicly available. Any application using EJS 3.1.6 or earlier that passes user-influenced data to template rendering options could be completely compromised.

Looking at the vulnerable dependency declaration:

"node_modules/ejs": {
  "version": "3.1.6",
  "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
  "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
  "dependencies": {
    "jake": "^10.6.1"
  }
}

This version contains the vulnerable code path that fails to sanitize the outputFunctionName option during template compilation.

The Vulnerability Explained

What is Server-Side Template Injection?

EJS is a popular templating engine for Node.js that lets developers embed JavaScript code within HTML templates. When you render a template, EJS compiles it into a JavaScript function that generates the final HTML output.

The vulnerability exists in how EJS handles the outputFunctionName option. This option allows developers to customize the name of the function that writes output during template rendering. In vulnerable versions, EJS directly concatenates this option value into generated JavaScript code without proper sanitization.

How the Attack Works

An attacker can craft a malicious outputFunctionName value that breaks out of the intended context and injects arbitrary JavaScript. Consider this attack payload:

// Malicious request to a vulnerable endpoint
GET /page?settings[view%20options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('id');s

// Or via POST body
{
  "settings": {
    "view options": {
      "outputFunctionName": "x;process.mainModule.require('child_process').execSync('whoami');s"
    }
  }
}

When EJS compiles a template with this poisoned option, it generates code like:

var x;process.mainModule.require('child_process').execSync('whoami');s = "";
// ... rest of template code

The injected code executes during template compilation, giving the attacker full control over the server. They can:

  • Execute system commands
  • Read sensitive files (environment variables, credentials)
  • Establish reverse shells
  • Pivot to internal network resources
  • Modify or delete data

Real-World Impact for This Application

The react-redux-bad-algo project is a React application with server-side rendering capabilities. If this application:

  1. Uses EJS for any server-side templating (email templates, PDF generation, SSR)
  2. Allows any user input to influence render options (through query parameters, request bodies, or configuration)

Then an attacker could achieve remote code execution on the production server hosting this application.

The Fix

The fix is elegantly simple: upgrade EJS from version 3.1.6 to 3.1.7. The EJS maintainers added proper validation to the outputFunctionName option, rejecting any values that contain characters that could enable code injection.

Before (Vulnerable)

"node_modules/ejs": {
  "version": "3.1.6",
  "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
  "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
  "dependencies": {
    "jake": "^10.6.1"
  }
}

After (Fixed)

"node_modules/ejs": {
  "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"
  }
}

What Changed in EJS 3.1.7

The patched version adds validation that checks outputFunctionName against a strict pattern, ensuring it can only contain valid JavaScript identifier characters. Any attempt to inject semicolons, parentheses, or other code-breaking characters is now rejected before template compilation begins.

The fix also updated the jake dependency from ^10.6.1 to ^10.8.5, and transitively updated filelist from 1.0.2 to 1.0.6 with its minimatch dependency, ensuring the entire dependency chain is secure.

Changes Made

Two files were modified:

  1. react-redux-bad-algo/package.json: Added explicit EJS dependency at version ^3.1.7
  2. react-redux-bad-algo/package-lock.json: Updated the resolved EJS version and all transitive dependencies

By explicitly declaring EJS in package.json, the project ensures that even if other dependencies try to pull in an older version, npm will resolve to the safe 3.1.7+ version.

Key Takeaways

  • EJS 3.1.6's outputFunctionName option was vulnerable to code injection because it concatenated user input directly into generated JavaScript without validation
  • The react-redux-bad-algo project's package-lock.json pinned the vulnerable version, making the application susceptible to RCE attacks
  • Upgrading to EJS 3.1.7 adds input validation that rejects malicious characters in template options
  • Explicitly declaring dependencies in package.json (not just relying on transitive dependencies) gives you control over which versions are installed
  • Automated dependency scanning with tools like Trivy can catch these vulnerabilities before they reach production

How Orbis AppSec Detected This

  • Source: The package-lock.json dependency declaration that resolved EJS to version 3.1.6
  • Sink: The EJS template compilation function that processes outputFunctionName without sanitization
  • Missing control: No validation of the outputFunctionName option before code generation
  • CWE: CWE-94 (Improper Control of Generation of Code)
  • Fix: Upgraded EJS from 3.1.6 to 3.1.7, which adds proper validation to reject injection attempts in template options

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 how a single unsanitized configuration option can turn a widely-used template engine into a remote code execution vector. The vulnerability in EJS's outputFunctionName handling allowed attackers to break out of the template context and execute arbitrary JavaScript on the server.

The fix was straightforward—a version bump from 3.1.6 to 3.1.7—but the impact of leaving it unpatched could have been catastrophic. This case reinforces the importance of:

  1. Automated dependency scanning in your CI/CD pipeline
  2. Prompt patching of known vulnerabilities
  3. Understanding how your dependencies handle user input
  4. Defense in depth, because even trusted libraries can have critical flaws

Keep your dependencies updated, audit your template rendering code paths, and never let user input control template engine configuration.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #185

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.