CVE-2022-29078
Description
EJS 3.1.6 is vulnerable to server-side template injection via the outputFunctionName option, allowing remote code execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
EJS 3.1.6 is vulnerable to server-side template injection via the outputFunctionName option, allowing remote code execution.
Vulnerability
The ejs package version 3.1.6 for Node.js is vulnerable to server-side template injection. The render function merges user-controlled data into template options when the data is passed directly to res.render in Express applications. Specifically, an attacker can set the settings[view options][outputFunctionName] parameter, which overwrites the internal outputFunctionName option. This option is used in template compilation to generate JavaScript code, and if not sanitized, allows arbitrary code execution [1][2][3].
Exploitation
An attacker can send a crafted HTTP request to an Express endpoint that uses res.render with user input, such as res.render('page', req.query). By providing a query string like ?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('id');x, the attacker injects arbitrary JavaScript code. The injected code is executed during template compilation, leading to OS command execution. No authentication is required, and the attack can be performed over the network [3].
Impact
Successful exploitation allows remote code execution (RCE) with the privileges of the Node.js process. The attacker can execute arbitrary system commands, potentially leading to full server compromise, data exfiltration, or further lateral movement within the network [2][3].
Mitigation
The vulnerability is fixed in ejs version 3.1.7, which validates that outputFunctionName is a valid JavaScript identifier [4]. Users should upgrade to 3.1.7 or later immediately. For applications that cannot upgrade, avoid passing user input directly to res.render and ensure only trusted data is used as template options. No reliable workaround exists for the vulnerable version [1][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ejsnpm | < 3.1.7 | 3.1.7 |
Affected products
2- ejs/Embedded JavaScript templatesdescription
Patches
12 files changed · +36 −0
lib/ejs.js+10 −0 modified@@ -64,6 +64,7 @@ var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compi // so we make an exception for `renderFile` var _OPTS_PASSABLE_WITH_DATA_EXPRESS = _OPTS_PASSABLE_WITH_DATA.concat('cache'); var _BOM = /^\uFEFF/; +var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; /** * EJS template function cache. This can be a LRU object from lru-cache NPM @@ -587,12 +588,21 @@ Template.prototype = { ' var __output = "";\n' + ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n'; if (opts.outputFunctionName) { + if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) { + throw new Error('outputFunctionName is not a valid JS identifier.'); + } prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; } + if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) { + throw new Error('localsName is not a valid JS identifier.'); + } if (opts.destructuredLocals && opts.destructuredLocals.length) { var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n'; for (var i = 0; i < opts.destructuredLocals.length; i++) { var name = opts.destructuredLocals[i]; + if (!_JS_IDENTIFIER.test(name)) { + throw new Error(`destructuredLocals[${i}] is not a valid JS identifier.`); + } if (i > 0) { destructuring += ',\n '; }
test/ejs.js+26 −0 modified@@ -1178,3 +1178,29 @@ suite('meta information', function () { assert.strictEqual(ejs.name, 'ejs'); }); }); + +suite('identifier validation', function () { + test('invalid outputFunctionName', function() { + assert.throws(function() { + ejs.compile('<p>yay</p>', {outputFunctionName: 'x;console.log(1);x'}); + }, /outputFunctionName is not a valid JS identifier/) + }); + + test('invalid localsName', function() { + var locals = Object.create(null); + assert.throws(function() { + ejs.compile('<p>yay</p>', { + localsName: 'function(){console.log(1);return locals;}()'}); + }, /localsName is not a valid JS identifier/) + }); + + test('invalid destructuredLocals', function() { + var locals = {}; + assert.throws(function() { + ejs.compile('<p>yay</p>', { + destructuredLocals: [ + 'console.log(1); //' + ]}); + }, /destructuredLocals\[0\] is not a valid JS identifier/) + }); +}); \ No newline at end of file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-phwq-j96m-2c2qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-29078ghsaADVISORY
- eslam.io/posts/ejs-server-side-template-injection-rceghsaWEB
- eslam.io/posts/ejs-server-side-template-injection-rce/mitrex_refsource_MISC
- github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24bafghsaWEB
- github.com/mde/ejs/releasesghsax_refsource_MISCWEB
- security.netapp.com/advisory/ntap-20220804-0001ghsaWEB
- security.netapp.com/advisory/ntap-20220804-0001/mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.