From 3869f90fccab42bd1bd17b8fff556a50464fb55d Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 26 Aug 2020 21:01:24 +0800 Subject: [PATCH] server: allow restriction on file operations by paths Bug: Flood-UI/flood#588 --- config.cli.js | 5 +++++ config.template.js | 4 +++- server/models/ClientRequest.js | 11 ---------- server/models/Filesystem.js | 16 ++++++++++---- server/models/client.js | 30 ++++++++++++++++++++------ server/util/fileUtil.js | 39 ++++++++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 22 deletions(-) create mode 100644 server/util/fileUtil.js diff --git a/config.cli.js b/config.cli.js index 83369c4b..3335e5e6 100644 --- a/config.cli.js +++ b/config.cli.js @@ -68,6 +68,10 @@ const {argv} = require('yargs') hidden: true, type: 'string', }) + .option('allowedpath', { + describe: 'Allowed path for file operations, can be called multiple times', + type: 'string', + }) .option('dbclean', { default: 1000 * 60 * 60, describe: 'ADVANCED: Interval between database purge', @@ -144,6 +148,7 @@ const CONFIG = { ssl: argv.ssl, sslKey: argv.sslkey || path.resolve(path.join(argv.rundir, 'key.pem')), sslCert: argv.sslcert || path.resolve(path.join(argv.rundir, 'fullchain.pem')), + allowedPaths: argv.allowedpath ? [].concat(argv.allowedpath) : null, }; module.exports = CONFIG; diff --git a/config.template.js b/config.template.js index 5359b845..ba80ca8c 100644 --- a/config.template.js +++ b/config.template.js @@ -67,7 +67,9 @@ const CONFIG = { // watchMountPoints: [ // "/mnt/disk" // ] - } + }, + // Allowed paths for file operations + // allowedPaths: ['/mnt/download', '/data/download'], }; // Do not remove the below line. module.exports = CONFIG; diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index c4312fb8..f30af58d 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -67,17 +67,6 @@ class ClientRequest { } } - // TODO: Move this to util, doesn't belong here - createDirectory(options) { - if (options.path) { - fs.mkdir(options.path, {recursive: true}, (error) => { - if (error) { - console.trace('Error creating directory.', error); - } - }); - } - } - clearRequestQueue() { this.requests = []; } diff --git a/server/models/Filesystem.js b/server/models/Filesystem.js index a24bed64..41221ae1 100644 --- a/server/models/Filesystem.js +++ b/server/models/Filesystem.js @@ -2,15 +2,23 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); +const fileUtil = require('../util/fileUtil'); + const getDirectoryList = (options, callback) => { const sourcePath = (options.path || '/').replace(/^~/, os.homedir()); + const resolvedPath = path.resolve(sourcePath); + if (!fileUtil.isAllowedPath(resolvedPath)) { + callback(null, fileUtil.accessDeniedError()); + return; + } + try { const directories = []; const files = []; - fs.readdirSync(sourcePath).forEach((item) => { - const joinedPath = path.join(sourcePath, item); + fs.readdirSync(resolvedPath).forEach((item) => { + const joinedPath = path.join(resolvedPath, item); if (fs.existsSync(joinedPath)) { if (fs.statSync(joinedPath).isDirectory()) { directories.push(item); @@ -20,13 +28,13 @@ const getDirectoryList = (options, callback) => { } }); - const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(sourcePath); + const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(resolvedPath); callback({ directories, files, hasParent, - path: sourcePath, + path: resolvedPath, separator: path.sep, }); } catch (error) { diff --git a/server/models/client.js b/server/models/client.js index f5634f2a..9bb5820b 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -6,6 +6,7 @@ const tar = require('tar-stream'); const ClientRequest = require('./ClientRequest'); const clientResponseUtil = require('../util/clientResponseUtil'); const clientSettingsMap = require('../../shared/constants/clientSettingsMap'); +const fileUtil = require('../util/fileUtil'); const settings = require('./settings'); const torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap'); const torrentPeerPropsMap = require('../../shared/constants/torrentPeerPropsMap'); @@ -23,7 +24,13 @@ const client = { tags = tags.split(','); } - request.createDirectory({path: destinationPath}); + const resolvedPath = path.resolve(destinationPath); + if (!fileUtil.isAllowedPath(resolvedPath)) { + callback(null, fileUtil.accessDeniedError()); + return; + } + + fileUtil.createDirectory({path: resolvedPath}); request.send(); // Each torrent is sent individually because rTorrent accepts a total @@ -35,7 +42,7 @@ const client = { const fileRequest = new ClientRequest(user, services); fileRequest.addFiles({ files: file, - path: destinationPath, + path: resolvedPath, isBasePath, start, tags, @@ -58,10 +65,15 @@ const client = { addUrls(user, services, data, callback) { const {urls, destination, isBasePath, start, tags} = data; const request = new ClientRequest(user, services); - request.createDirectory({path: destination}); + const resolvedPath = path.resolve(destination); + if (!fileUtil.isAllowedPath(resolvedPath)) { + callback(null, fileUtil.accessDeniedError()); + return; + } + fileUtil.createDirectory({path: resolvedPath}); request.addURLs({ urls, - path: destination, + path: resolvedPath, isBasePath, start, tags, @@ -230,6 +242,12 @@ const client = { const {isBasePath, hashes, filenames, moveFiles, sourcePaths, isCheckHash} = data; const mainRequest = new ClientRequest(user, services); + const resolvedPath = path.resolve(destinationPath); + if (!fileUtil.isAllowedPath(resolvedPath)) { + callback(null, fileUtil.accessDeniedError()); + return; + } + const hashesToRestart = hashes.filter( (hash) => !services.torrentService.getTorrent(hash).status.includes(torrentStatusMap.stopped), ); @@ -266,7 +284,7 @@ const client = { moveTorrentsRequest.moveTorrents({ filenames, sourcePaths, - destinationPath, + resolvedPath, }); }; @@ -277,7 +295,7 @@ const client = { } mainRequest.stopTorrents({hashes}); - mainRequest.setDownloadPath({hashes, path: destinationPath, isBasePath}); + mainRequest.setDownloadPath({hashes, path: resolvedPath, isBasePath}); mainRequest.onComplete(afterSetPath); mainRequest.send(); }, diff --git a/server/util/fileUtil.js b/server/util/fileUtil.js new file mode 100644 index 00000000..46a0bdff --- /dev/null +++ b/server/util/fileUtil.js @@ -0,0 +1,39 @@ +const fs = require('fs'); + +const config = require('../../config'); + +const createDirectory = (options) => { + if (options.path) { + fs.mkdir(options.path, {recursive: true}, (error) => { + if (error) { + console.trace('Error creating directory.', error); + } + }); + } +}; + +const isAllowedPath = (resolvedPath) => { + if (config.allowedPaths == null) { + return true; + } + return config.allowedPaths.some((allowedPath) => { + if (resolvedPath.startsWith(allowedPath)) { + return true; + } + return false; + }); +}; + +const accessDeniedError = () => { + const error = new Error(); + error.code = 'EACCES'; + return error; +}; + +const fileUtil = { + createDirectory, + isAllowedPath, + accessDeniedError, +}; + +module.exports = fileUtil;