VYPR
Critical severityNVD Advisory· Published Apr 25, 2022· Updated Aug 3, 2024

CVE-2022-29078

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.

PackageAffected versionsPatched versions
ejsnpm
< 3.1.73.1.7

Affected products

2
  • ejs/Embedded JavaScript templatesdescription
  • ghsa-coords
    Range: < 3.1.7

Patches

1
15ee698583c9

Sanitize option names.

https://github.com/mde/ejsNicolas DumazetMay 30, 2021via ghsa
2 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

News mentions

0

No linked articles in our index yet.