From 74db3e17d075f23d9c95d7871586cf461524c456 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 1 Feb 2021 13:39:39 +0000 Subject: [PATCH] Restrict project file access to inside the project directory --- .../localfilesystem/projects/Project.js | 20 ++++++++++++++++++- .../storage/localfilesystem/projects/index.js | 19 ++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js index 0fbf89de4..4cef78b04 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js @@ -305,6 +305,9 @@ Project.prototype.update = function (user, data) { return Promise.reject("Invalid package file: "+data.files.package) } var root = data.files.package.substring(0,data.files.package.length-12); + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.package)))) { + return Promise.reject("Invalid package file: "+data.files.package) + } this.paths.root = root; this.paths['package.json'] = data.files.package; globalProjectSettings.projects[this.name].rootPath = root; @@ -322,12 +325,18 @@ Project.prototype.update = function (user, data) { } if (data.files.hasOwnProperty('flow') && this.package['node-red'].settings.flowFile !== data.files.flow.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.flow)))) { + return Promise.reject("Invalid flow file: "+data.files.flow) + } this.paths.flowFile = data.files.flow; this.package['node-red'].settings.flowFile = data.files.flow.substring(this.paths.root.length); savePackage = true; flowFilesChanged = true; } if (data.files.hasOwnProperty('credentials') && this.package['node-red'].settings.credentialsFile !== data.files.credentials.substring(this.paths.root.length)) { + if (/^\.\./.test(fspath.relative(this.path,fspath.join(this.path,data.files.credentials)))) { + return Promise.reject("Invalid credentials file: "+data.files.credentials) + } this.paths.credentialsFile = data.files.credentials; this.package['node-red'].settings.credentialsFile = data.files.credentials.substring(this.paths.root.length); // Don't know if the credSecret is invalid or not so clear the flag @@ -490,6 +499,10 @@ Project.prototype.getFile = function (filePath,treeish) { if (treeish !== "_") { return gitTools.getFile(this.path, filePath, treeish); } else { + let fullPath = fspath.join(this.path,filePath); + if (/^\.\./.test(fspath.relative(this.path,fullPath))) { + throw new Error("Invalid file name") + } return fs.readFile(fspath.join(this.path,filePath),"utf8"); } }; @@ -639,6 +652,11 @@ Project.prototype.pull = function (user,remoteBranchName,setRemote,allowUnrelate Project.prototype.resolveMerge = function (file,resolutions) { var filePath = fspath.join(this.path,file); + + if (/^\.\./.test(fspath.relative(this.path,filePath))) { + throw new Error("Invalid file name") + } + var self = this; if (typeof resolutions === 'string') { return util.writeFile(filePath, resolutions).then(function() { @@ -1062,7 +1080,7 @@ function loadProject(projectPath) { function init(_settings, _runtime) { settings = _settings; runtime = _runtime; - projectsDir = fspath.join(settings.userDir,"projects"); + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); authCache.init(); } diff --git a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js index 5e1d4f15b..24f18b097 100644 --- a/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js +++ b/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js @@ -110,7 +110,7 @@ function init(_settings, _runtime) { globalGitUser = gitConfig.user; Projects.init(settings,runtime); sshTools.init(settings); - projectsDir = fspath.join(settings.userDir,"projects"); + projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects")); if (!settings.readOnly) { return fs.ensureDir(projectsDir) //TODO: this is accessing settings from storage directly as settings @@ -207,9 +207,16 @@ function getBackupFilename(filename) { } function loadProject(name) { + let fullPath = fspath.resolve(fspath.join(projectsDir,name)); var projectPath = name; if (projectPath.indexOf(fspath.sep) === -1) { - projectPath = fspath.join(projectsDir,name); + projectPath = fullPath; + } else { + // Ensure this project dir is under projectsDir; + let relativePath = fspath.relative(projectsDir,fullPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } } return Projects.load(projectPath).then(function(project) { activeProject = project; @@ -234,6 +241,10 @@ function deleteProject(user, name) { throw e; } var projectPath = fspath.join(projectsDir,name); + let relativePath = fspath.relative(projectsDir,projectPath); + if (/^\.\./.test(relativePath)) { + throw new Error("Invalid project name") + } return Projects.delete(user, projectPath); } @@ -392,6 +403,10 @@ function createProject(user, metadata) { metadata.files.credentialSecret = currentEncryptionKey; } metadata.path = fspath.join(projectsDir,metadata.name); + if (/^\.\./.test(fspath.relative(projectsDir,metadata.path))) { + throw new Error("Invalid project name") + } + return Projects.create(user, metadata).then(function(p) { return setActiveProject(user, p.name); }).then(function() {