CVE-2017-16083 (JavaScript)

node-simple-router's static file serving logic is vulnerable to directory traversal. An attacker who can send HTTP requests to the router can control the requested URL path and include raw or encoded dot-dot path segments, or other path forms that normalize outside the configured static_route directory. If the router builds a filesystem path from that URL without enforcing a canonical, path-boundary-aware containment check, the request can read and return files outside the intended static document root, including files in sibling directories whose names share the static root's string prefix. This can expose sensitive server-side files.

Weakness:
CWE-73: name: External Control of File Name or Path
description: The product allows user input to control or influence paths or file names that are used in filesystem operations.
CWE-22: name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Repository: https://github.com/sandy98/node-simple-router

Vulnerable code:
file_path: lib/router.js
snippet:     dispatch["static"] = function(pathname, req, res) {
      var full_path;
      full_path = "" + dispatch.static_route + (unescape(pathname));
      return fs.exists(full_path, function(exists) {
        var e, error;
        if (exists) {
          if (((pathname.indexOf(dispatch.cgi_dir + "/") !== -1) || (pathname.match(/\.php$/))) && (pathname.substr(-1) !== "/") && (dispatch.serve_cgi === true)) {
            try {
              return dispatch.cgi(pathname, req, res);
            } catch (error) {
              e = error;
              if (!!dispatch.logging) {
                dispatch.log(e.toString());
              }
              return dispatch._500(null, res, pathname);
            }
          } else {
            return fs.stat(full_path, function(err, stats) {
              var fd;
              if (err) {
                if (!!dispatch.logging) {
                  dispatch.log(err.toString());
                }
                return dispatch._500(null, res, pathname);
              }
              if (stats) {
                if (stats.isDirectory()) {
                  if (!!dispatch.list_dir) {
                    return dispatch.directory(full_path, pathname, res);
                  }
                  return dispatch._405(null, res, pathname, "Directory listing not allowed");
                }
                if (stats.isFile()) {
                  fd = fs.createReadStream(full_path);
                  res.writeHead(200, {
                    'Content-Type': mime_types[path_tools.extname(full_path)] || 'text/plain'
                  });
                  return fd.pipe(res);
                }
              }
            });
          }
        } else {
          if (unescape(pathname).match(/favicon\.ico$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('favicon.ico')] || 'application/x-icon'
            });
            return res.end(new Buffer(unescape(escaped_favicon), 'binary'));
          }
          if (unescape(pathname).match(/icons\.png$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('icons.png')] || 'image/png'
            });
            return res.end(new Buffer(unescape(escaped_icons_png), 'binary'));
          }
          if (unescape(pathname).match(/pixel\.gif$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('pixel.gif')] || 'image/gif'
            });
            return res.end(new Buffer(unescape(escaped_pixel_gif), 'binary'));
          } else {
            return dispatch._404(null, res, pathname);
          }
        }
      });
    };

Fixed code:
file_path: lib/router.js
snippet:     dispatch["static"] = function(pathname, req, res) {
      var full_path;
      full_path = "" + dispatch.static_route + (unescape(pathname));
      if (full_path.indexOf('..') !== -1) {
        return dispatch._403(null, res, pathname, "Trying to get private things through directory traversal is a nasty thing to do.");
      }
      return fs.exists(full_path, function(exists) {
        var e, error;
        if (exists) {
          if (((pathname.indexOf(dispatch.cgi_dir + "/") !== -1) || (pathname.match(/\.php$/))) && (pathname.substr(-1) !== "/") && (dispatch.serve_cgi === true)) {
            try {
              return dispatch.cgi(pathname, req, res);
            } catch (error) {
              e = error;
              if (!!dispatch.logging) {
                dispatch.log(e.toString());
              }
              return dispatch._500(null, res, pathname);
            }
          } else {
            return fs.stat(full_path, function(err, stats) {
              var fd;
              if (err) {
                if (!!dispatch.logging) {
                  dispatch.log(err.toString());
                }
                return dispatch._500(null, res, pathname);
              }
              if (stats) {
                if (stats.isDirectory()) {
                  if (!!dispatch.list_dir) {
                    return dispatch.directory(full_path, pathname, res);
                  }
                  return dispatch._405(null, res, pathname, "Directory listing not allowed");
                }
                if (stats.isFile()) {
                  fd = fs.createReadStream(full_path);
                  res.writeHead(200, {
                    'Content-Type': mime_types[path_tools.extname(full_path)] || 'text/plain'
                  });
                  return fd.pipe(res);
                }
              }
            });
          }
        } else {
          if (unescape(pathname).match(/favicon\.ico$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('favicon.ico')] || 'application/x-icon'
            });
            return res.end(new Buffer(unescape(escaped_favicon), 'binary'));
          }
          if (unescape(pathname).match(/icons\.png$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('icons.png')] || 'image/png'
            });
            return res.end(new Buffer(unescape(escaped_icons_png), 'binary'));
          }
          if (unescape(pathname).match(/pixel\.gif$/)) {
            res.writeHead(200, {
              'Content-Type': mime_types[path_tools.extname('pixel.gif')] || 'image/gif'
            });
            return res.end(new Buffer(unescape(escaped_pixel_gif), 'binary'));
          } else {
            return dispatch._404(null, res, pathname);
          }
        }
      });
    };