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.

Prevention & Best Practices

1. Never Trust User Input in Template Options

The safest approach is to never allow user-controlled data to influence template engine configuration:

// DANGEROUS: User input in render options
app.get('/page', (req, res) => {
  res.render('template', req.query.data, req.query.options); // NO!
});

// SAFE: Hardcoded options, user data only in template context
app.get('/page', (req, res) => {
  const safeData = sanitize(req.query.data);
  res.render('template', { userData: safeData }); // Options controlled by developer
});

2. Keep Dependencies Updated

Use automated tools to monitor and update dependencies:

# Check for known vulnerabilities
npm audit

# Update to patched versions
npm audit fix

# Use tools like Dependabot or Renovate for automated PRs

3. Use Lockfiles Correctly

Always commit your package-lock.json and use npm ci in CI/CD pipelines to ensure reproducible builds with exact versions.

4. Implement Defense in Depth

Even with updated dependencies:

  • Run Node.js with minimal privileges
  • Use containers with read-only filesystems where possible
  • Implement network segmentation
  • Monitor for suspicious process execution

5. Consider Template Engine Alternatives

For high-security applications, consider template engines with sandboxed execution or no code execution capabilities (like Mustache/Handlebars in logic-less mode).

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.

References

Frequently Asked Questions

What is server-side template injection?

Server-side template injection (SSTI) occurs when user input is embedded into a template engine in an unsafe way, allowing attackers to inject template directives that execute arbitrary code on the server.

How do you prevent server-side template injection in Node.js?

Prevent SSTI by never passing user-controlled input to template engine options, keeping template engines updated, using sandboxed rendering environments, and validating all inputs that influence template compilation.

What CWE is server-side template injection?

Server-side template injection is classified under CWE-94 (Improper Control of Generation of Code) and sometimes CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine).

Is input validation enough to prevent server-side template injection?

Input validation alone is often insufficient because template injection payloads can be obfuscated. The safest approach is to never allow user input to control template options like `outputFunctionName`, and to keep your template engine updated.

Can static analysis detect server-side template injection?

Yes, static analysis tools like Trivy, Snyk, and Semgrep can detect known vulnerable versions of template engines and flag dangerous patterns where user input flows into template compilation options.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #185

Related Articles

high

How Denial of Service via malformed HTTP header decoding happens in Node.js @opentelemetry/propagator-jaeger and how to fix it

CVE-2026-59892 is a high-severity Denial of Service vulnerability in `@opentelemetry/propagator-jaeger` versions prior to 2.9.0, where malformed HTTP trace-context headers could cause the propagator's decoding logic to crash a Node.js application. The fix upgrades the package from 2.8.0 to 2.9.0, patching the unsafe header parsing behavior and eliminating the attack surface for any service that propagates distributed tracing headers.

critical

How hardcoded API keys happen in Node.js client-side JavaScript and how to fix it

A critical hardcoded API key for ipregistry.co was discovered embedded as a fallback value in `src/utils.js` at line 65 of a Node.js library. This key (`f8n4kqe8pv4kii`) was visible to anyone inspecting the JavaScript bundle, allowing unauthorized use of the service. The fix removes the hardcoded fallback, requiring callers to explicitly provide their own API key.

critical

How unauthenticated API access happens in Node.js Express endpoints and how to fix it

The `/api/translate` endpoint in `api/translate.js` was publicly accessible without any authentication, allowing anonymous users to freely invoke paid Anthropic Claude or Google Translate API calls. This critical vulnerability exposed the application to API cost abuse, quota exhaustion, and potential data exfiltration. A targeted fix adds a shared-secret header check before any translation logic executes.

high

How HTTP Transport Hijacking via Prototype Pollution happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42033) in axios 1.13.6 allowed attackers to hijack HTTP transport behavior through prototype pollution, potentially redirecting sensitive requests to attacker-controlled servers. The fix upgrades axios to 1.15.1 and proxy-from-env to 2.1.0 in a Node.js sandbox base image used in production, eliminating the attack vector.

critical

How Denial of Service via gzip bomb happens in Node.js tar and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) was discovered in the node-tar package where attackers could craft malicious gzip archives that expand to consume all available system resources. This vulnerability affected version 7.5.15 of the tar package and was fixed by upgrading to version 7.5.19. The fix protects applications from resource exhaustion attacks when processing untrusted archive files.

critical

How session fixation happens in PHP OAuth login handlers and how to fix it

A session fixation vulnerability in `trunk/web/login_weibo.php` allowed attackers to hijack authenticated user sessions by pre-setting a victim's PHP session ID before they logged in via Weibo OAuth. The fix was a single, critical call to `session_regenerate_id(true)` inserted immediately before assigning the authenticated user's ID to the session. Left unpatched, this vulnerability could have given an attacker full access to any account whose Weibo OAuth login they could observe or influence.