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:
-
Parameter Sanitization: The
outputFunctionNameparameter now undergoes strict validation to ensure it only contains valid JavaScript identifier characters (alphanumerics,$,_). -
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.
-
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
overridesfield explicitly pins the safe version, ensuring this fix applies across the entire dependency tree and survives futurenpm installoperations. - 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
outputFunctionNameparameter 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
overridesfield 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
- CWE-94: Improper Control of Generation of Code
- CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
- OWASP: Server-Side Template Injection (SSTI)
- EJS Official Documentation
- CVE-2022-29078 NVD Entry
- Semgrep Rule: Unsafe Template Injection
- GitHub PR: fix: upgrade ejs to 3.1.7 (CVE-2022-29078)