188 lines
6.3 KiB
JavaScript
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;
|