Fixing a Remote Code Execution Vulnerability in EJS
Tim Kadlec
November 30, 2016
0 mins readThis week we added a high-severity Remote Code Execution vulnerability in the EJS package to our vulnerability database.
EJS (Embedded JavaScript Templates) is a fast, simple and very popular JavaScript templating engine. EJS provides a few different options for you to render a template. Two of them, render
and renderFile
are fairly similar, the only difference being that render
expects a string to be used for the template and renderFile
expects a path to a template file. Both methods also accept arguments for the data and an optional set of configuration options. The renderFile
method also accepts a callback function.
1ejs.render(str, data, options);
2
3ejs.renderFile(filename, data, options, callback)
Both methods also allow you to roll the data and options together into one single object.
1ejs.render(str, dataAndOptions);
2
3ejs.renderFile(filename, dataAndOptions, callback)
The Vulnerability
Using the shortcut method may seem easier, but mixing in data and options could be error prone over time. If that’s not enough to sway you off it, here’s a better reason: using the shortcut method can expose your application to a remote code execution vulnerability.
The EJS templates that you compile can include other files by using an include directive.
1<%- include('path/to/include'); %>
By default, these includes are relative to the template. If the template is in the templates
folder, then EJS will look in templates/path/to/include
to find the file to include.
One of the configuration options available to you is the root
option, which enables you to change the default location from which those includes are being pulled.
This isn’t a problem when your options are passed as a separate argument but it does become a problem if you combine your options and data into a single object. If the method passes along a list of data supplied by the user, an attacker could intercept and inject a root
option as well.
1ejs.renderFile('my-template', {root:'/bad/root/'}, callback);
By passing along the root
directive in the line above, any includes would now be pulled from /bad/root
instead of the path intended, resulting in remote code execution.
While being able to configure the root can be useful for developers using the EJS engine, allowing it to be configured in an object alongside user data presents a serious security risk. The developer could indeed take some steps to sanitize the input, but the engine itself would be insecure by default.
Our security team found the issue and disclosed it on November 27th. The project’s owner, Matthew Eernisse, put together a simple fix that blacklists the root
option so that it can’t be included in alongside user data.
Frequently, vulnerabilities remain unfixed long after their initial disclosure. In fact, if you recall the XSS vulnerability in Marked, that took over a year to be addressed. Thankfully that wasn’t the case here. Thanks to a remarkably quick release from Matthew, the vulnerability went from disclosed to fixed in only one day. Version 2.5.3 of EJS, released on November 28th, includes the fix for the vulnerability.
How to fix it
If you’ve told Snyk to watch your project and it uses EJS, you likely already received an alert about the issue. The vulnerability can be resolved by either using the GitHub integration to generate a pull-request from your dashboard or by running snyk wizard
from the command-line interface. In either case, Snyk will identify the issue and prompt you to update the EJS package to the latest version.
If you’re not running Snyk — it’s ok, we still love you — you can address the issue by manually updating to the latest version of EJS. Just be sure to check all of your dependencies as well. If one of them is pulling in the EJS package, that won’t show up in your package.json
file and updating may be a much more involved process.
Get started in capture the flag
Learn how to solve capture the flag challenges by watching our virtual 101 workshop on demand.