diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index b5005f0ca..f451d6378 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -12,6 +12,7 @@ const getMapExport = require('../utility/getMapExport'); const dbgateApi = require('../shell'); const { getLogger } = require('dbgate-tools'); const platformInfo = require('../utility/platformInfo'); +const { checkSecureFilePaths, checkSecureDirectories } = require('../utility/security'); const logger = getLogger('files'); function serialize(format, data) { @@ -26,25 +27,6 @@ function deserialize(format, text) { throw new Error(`Invalid format: ${format}`); } -function checkSecureFilePaths(...filePaths) { - for (const filePath of filePaths) { - if (filePath.includes('..') || filePath.includes('/') || filePath.includes('\\')) { - return false; - } - } - return true; -} - -function checkSecureDirectories(...filePaths) { - for (const filePath of filePaths) { - const directory = path.dirname(filePath); - if (directory != filesdir() && directory != uploadsdir() && directory != archivedir() && directory != appdir()) { - return false; - } - } - return true; -} - module.exports = { list_meta: true, async list({ folder }, req) { diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js index 5ed9bbea1..3ee287953 100644 --- a/packages/api/src/controllers/runners.js +++ b/packages/api/src/controllers/runners.js @@ -19,6 +19,7 @@ const { const { handleProcessCommunication } = require('../utility/processComm'); const processArgs = require('../utility/processArgs'); const platformInfo = require('../utility/platformInfo'); +const { checkSecureDirectories, checkSecureDirectoriesInScript } = require('../utility/security'); const logger = getLogger('runners'); function extractPlugins(script) { @@ -273,6 +274,12 @@ module.exports = { const runid = crypto.randomUUID(); if (script.type == 'json') { + if (!platformInfo.isElectron) { + if (!checkSecureDirectoriesInScript(script)) { + return { errorMessage: 'Unallowed directories in script' }; + } + } + const js = await jsonScriptToJavascript(script); return this.startCore(runid, scriptTemplate(js, false)); } @@ -317,6 +324,11 @@ module.exports = { loadReader_meta: true, async loadReader({ functionName, props }) { + if (!platformInfo.isElectron) { + if (props?.fileName && !checkSecureDirectories(props.fileName)) { + return { errorMessage: 'Unallowed file' }; + } + } const prefix = extractShellApiPlugins(functionName) .map(packageName => `// @require ${packageName}\n`) .join(''); diff --git a/packages/api/src/utility/security.js b/packages/api/src/utility/security.js new file mode 100644 index 000000000..bfac56b21 --- /dev/null +++ b/packages/api/src/utility/security.js @@ -0,0 +1,52 @@ +const path = require('path'); +const { filesdir, archivedir, uploadsdir, appdir } = require('../utility/directories'); + +function checkSecureFilePaths(...filePaths) { + for (const filePath of filePaths) { + if (filePath.includes('..') || filePath.includes('/') || filePath.includes('\\')) { + return false; + } + } + return true; +} + +function checkSecureDirectories(...filePaths) { + for (const filePath of filePaths) { + if (!filePath.includes('/') && !filePath.includes('\\')) { + // If the filePath does not contain any directory separators, it is considered secure + continue; + } + const directory = path.dirname(filePath); + if (directory != filesdir() && directory != uploadsdir() && directory != archivedir() && directory != appdir()) { + return false; + } + } + return true; +} + +function findDisallowedFileNames(node, isAllowed, trace = '$', out = []) { + if (node && typeof node === 'object') { + if (node?.props?.fileName) { + const name = node.props.fileName; + const ok = isAllowed(name); + if (!ok) out.push({ path: `${trace}.props.fileName`, value: name }); + } + + // depth-first scan of every property / array index + for (const [key, val] of Object.entries(node)) { + findDisallowedFileNames(val, isAllowed, `${trace}.${key}`, out); + } + } + return out; +} + +function checkSecureDirectoriesInScript(script) { + const disallowed = findDisallowedFileNames(script, checkSecureDirectories); + return disallowed.length == 0; +} + +module.exports = { + checkSecureDirectories, + checkSecureFilePaths, + checkSecureDirectoriesInScript, +}; diff --git a/packages/web/src/tabs/ImportExportTab.svelte b/packages/web/src/tabs/ImportExportTab.svelte index 1b167a93b..4ac1ae369 100644 --- a/packages/web/src/tabs/ImportExportTab.svelte +++ b/packages/web/src/tabs/ImportExportTab.svelte @@ -52,6 +52,7 @@ import ToolStripSaveButton from '../buttons/ToolStripSaveButton.svelte'; import uuidv1 from 'uuid/v1'; import { tick } from 'svelte'; + import { showSnackbarError } from '../utility/snackbar'; let busy = false; let executeNumber = 0; @@ -200,6 +201,11 @@ } else { let runid = runnerId; const resp = await apiCall('runners/start', { script }); + if (resp.errorMessage) { + busy = false; + showSnackbarError(resp.errorMessage); + return; + } runid = resp.runid; runnerId = runid; }