Simon Priet e69a613a37 feat: Created a mini nodeJS server with NewMan for testing without PostMan GUI.
This will mimic a run in a CD/CI environment or docker container.
2021-09-08 14:01:19 +02:00

188 lines
6.3 KiB
JavaScript

const fs = require('fs'),
_ = require('lodash'),
path = require('path'),
util = require('util'),
Readable = require('stream').Readable,
PPERM_ERR = 'PPERM: insecure file access outside working directory',
FUNCTION = 'function',
DEPRECATED_SYNC_WRITE_STREAM = 'SyncWriteStream',
EXPERIMENTAL_PROMISE = 'promises',
// Use simple character check instead of regex to prevent regex attack
/*
* Windows root directory can be of the following from
*
* | File System | Actual | Modified |
* |-------------|------------------|-------------------|
* | LFS (Local) | C:\Program | /C:/Program |
* | UNC | \\Server\Program | ///Server/Program |
*/
isWindowsRoot = function (path) {
const drive = path.charAt(1);
return ((path.charAt(0) === '/') &&
((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z')) &&
(path.charAt(2) === ':')) ||
path.slice(0, 3) === '///'; // Modified UNC path
},
stripTrailingSep = function (thePath) {
if (thePath[thePath.length - 1] === path.sep) {
return thePath.slice(0, -1);
}
return thePath;
},
pathIsInside = function (thePath, potentialParent) {
// For inside-directory checking, we want to allow trailing slashes, so normalize.
thePath = stripTrailingSep(thePath);
potentialParent = stripTrailingSep(potentialParent);
// Node treats only Windows as case-insensitive in its path module; we follow those conventions.
if (global.process.platform === 'win32') {
thePath = thePath.toLowerCase();
potentialParent = potentialParent.toLowerCase();
}
return thePath.lastIndexOf(potentialParent, 0) === 0 &&
(
thePath[potentialParent.length] === path.sep ||
thePath[potentialParent.length] === undefined
);
};
/**
* Secure file resolver wrapper over fs. It only allow access to files inside working directory unless specified.
*
* @param {*} workingDir - Path of working directory
* @param {*} [insecureFileRead=false] - If true, allow reading files outside working directory
* @param {*} [fileWhitelist=[]] - List of allowed files outside of working directory
*/
function SecureFS (workingDir, insecureFileRead = false, fileWhitelist = []) {
this._fs = fs;
this._path = path;
this.constants = this._fs.constants;
this.workingDir = workingDir;
this.insecureFileRead = insecureFileRead;
this.fileWhitelist = fileWhitelist;
this.isWindows = global.process.platform === 'win32';
}
/**
* Private method to resole the path based based on working directory
*
* @param {String} relOrAbsPath - Relative or absolute path to resolve
* @param {Array} whiteList - A list of absolute path to whitelist
*
* @returns {String} The resolved path
*/
SecureFS.prototype._resolve = function (relOrAbsPath, whiteList) {
// Special handling for windows absolute paths to work cross platform
this.isWindows && isWindowsRoot(relOrAbsPath) && (relOrAbsPath = relOrAbsPath.substring(1));
// Resolve the path from the working directory. The file should always be resolved so that
// cross os variations are mitigated
let resolvedPath = this._path.resolve(this.workingDir, relOrAbsPath);
// Check file is within working directory
if (!this.insecureFileRead && // insecureFile read disabled
!pathIsInside(resolvedPath, this.workingDir) && // File not inside working directory
!_.includes(whiteList, resolvedPath)) { // File not in whitelist
// Exit
return undefined;
}
return resolvedPath;
};
/**
* Asynchronous path resolver function
*
* @param {String} relOrAbsPath - Relative or absolute path to resolve
* @param {Array} [whiteList] - A optional list of additional absolute path to whitelist
* @param {Function} callback -
*/
SecureFS.prototype.resolvePath = function (relOrAbsPath, whiteList, callback) {
if (!callback && typeof whiteList === FUNCTION) {
callback = whiteList;
whiteList = [];
}
let resolvedPath = this._resolve(relOrAbsPath, _.concat(this.fileWhitelist, whiteList));
if (!resolvedPath) {
return callback(new Error(PPERM_ERR));
}
return callback(null, resolvedPath);
};
/**
* Synchronous path resolver function
*
* @param {String} relOrAbsPath - Relative or absolute path to resolve
* @param {Array} [whiteList] - A optional list of additional absolute path to whitelist
*
* @returns {String} The resolved path
*/
SecureFS.prototype.resolvePathSync = function (relOrAbsPath, whiteList) {
// Resolve the path from the working directory
const resolvedPath = this._resolve(relOrAbsPath, _.concat(this.fileWhitelist, whiteList));
if (!resolvedPath) {
throw new Error(PPERM_ERR);
}
return resolvedPath;
};
// Attach all functions in fs to postman-fs
Object.getOwnPropertyNames(fs).map((prop) => {
// Bail-out early to prevent fs module from logging warning for deprecated and experimental methods
if (prop === DEPRECATED_SYNC_WRITE_STREAM || prop === EXPERIMENTAL_PROMISE || typeof fs[prop] !== FUNCTION) {
return;
}
SecureFS.prototype[prop] = fs[prop];
});
// Override the required functions
SecureFS.prototype.stat = function (path, callback) {
this.resolvePath(path, (err, resolvedPath) => {
if (err) {
return callback(err);
}
return this._fs.stat(resolvedPath, callback);
});
};
SecureFS.prototype.createReadStream = function (path, options) {
try {
return this._fs.createReadStream(this.resolvePathSync(path), options);
}
catch (err) {
// Create a fake read steam that emits and error and
const ErrorReadStream = function () {
// Replicating behavior of fs module of disabling emitClose on destroy
Readable.call(this, { emitClose: false });
// Emit the error event with insure file access error
this.emit('error', new Error(PPERM_ERR));
// Options exists and disables autoClose then don't destroy
(options && !options.autoClose) || this.destroy();
};
util.inherits(ErrorReadStream, Readable);
return new ErrorReadStream();
}
};
module.exports = SecureFS;