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:
- Uses EJS for any server-side templating (email templates, PDF generation, SSR)
- 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:
react-redux-bad-algo/package.json: Added explicit EJS dependency at version^3.1.7react-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
outputFunctionNameoption was vulnerable to code injection because it concatenated user input directly into generated JavaScript without validation - The
react-redux-bad-algoproject'spackage-lock.jsonpinned 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.jsondependency declaration that resolved EJS to version 3.1.6 - Sink: The EJS template compilation function that processes
outputFunctionNamewithout sanitization - Missing control: No validation of the
outputFunctionNameoption 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:
- Automated dependency scanning in your CI/CD pipeline
- Prompt patching of known vulnerabilities
- Understanding how your dependencies handle user input
- 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.