• /lib/application.js#L548L610 • /lib/view.js#L52L95 • /lib/application.js#L655L661 • /lib/view.js#L133L136
• Code for Testing • Check ejs extension management logic • Exploit !!
Summary
Express.js, or simply Express, is a web framework for Node.js, released as free and open source software licensed under the MIT license. It is being called the de facto standard server framework of Node.js.
I started to analyze it. While analyzing several pieces of code, I found a way to trigger an RCE vulnerability via confusing file extenstion in the render() function.
The express framework internally calls template libraries such as ejs, Handlebars, and dot using the require() function. Confusion arises in this process.
Function call procedure
render() → View() → tryRender() → View.prototype.render → this.engine()
The analysis
/lib/application.js#L548L610
1 | app.render = function render(name, options, callback) { |
The render() function calls View function if the view variable is empty. And when the function ends, it calls tryRender() function.
/lib/view.js#L52L95
1 | /* |
The View() function makes an anonymous function. In some if statement, if the !opts.engines[this.ext] property is empty, after cutting the first letter from the value of this.ext, the value is used to call the require() function. At this time, the function code called __express
in the JavaScript file is imported and defined in opts.engines[this.ext]
. Then, define the value of opts.engines[this.ext]
in this.engine variable. That is, the this.engine
variable contains the __express function.

This is where the root cause of this vulnerability occurs. After parsing the extension using path.extname(), it does not check the extension. That’s all.
/lib/application.js#L655L661
1 | function tryRender(view, options, callback) { |
In the render() function, call the tryRender() function after calling the View() function. The tryRender() function calls the View.prototype.render() function
/lib/view.js#L133L136
1 | View.prototype.render = function render(options, callback) { |
Lastly, in the View.prototype.render() function, the anonymous function this.engine() function is executed.
How to trigger an RCE
Code for Testing
1 | const express = require('express') |
The test code is as above.
Check ejs extension management logic

I checked how the extension is managed in the logic that handles the extension of the file passed to the render() function. When I pass files like render(‘test’), render(‘test.ejs’), all extensions are ejs .

However, when the render() function is called like render(‘rce.pocas’), “pocas”, not “ejs”, is included in the extension. Since the engine type was set to “ejs” using app.set() in express, the extension should be ejs in any case, but an arbitrary extension can be inserted because there is no exception handling.
1 | var mod = this.ext.slice(1) |
That is, I can manipulate the extension and call the JavaScript library I want through the code above! Through the above function, get the __express function of the desired file, put it in this.engine variable, and execute this.engine() in view.prototype.render() function. If a hacker can upload a desired file under node_modules using the file upload function, the desired function code can be inserted into this.engine variable and executed.
Exploit !!

1 | exports.__express = function() { |
For the test, a module called pocas was created under node_modules.

As shown above, you can see that RCE is triggered by calling an arbitrary library using the extension confusing.
Mitigation
The reason why the vulnerability occurs is that the file extension is parsed using the path.extname() function and the extension is not checked. Since the file extension is not checked, other arbitrary modules other than the ejs module can be called. So add file extension checking logic.
:Recommendation: compare whether the extension obtained through extname() and the extension of the server’s default template are the same