CVE-2021-21298 (published 2021-02-26T17:15Z)

Node-Red is a low-code programming for event-driven applications built using nodejs. Node-RED 1.2.7 and earlier has a vulnerability which allows arbitrary path traversal via the Projects API. If the Projects feature is enabled, a user with `projects.read` permission is able to access any file via the Projects API. The issue has been patched in Node-RED 1.2.8. The vulnerability applies only to the Projects feature which is not enabled by default in Node-RED. The primary workaround is not give untrusted users read access to the Node-RED editor.

Severity: LOW
CVSS v2 base score: 3.5 (AV:N/AC:M/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/node-red/node-red
commit_details: repo_url: https://github.com/node-red/node-red
author: Nick O'Leary
author_date: 2021-02-01 13:39:39+00:00
msg: Restrict project file access to inside the project directory
repository: repo_url: https://github.com/node-red/node-red
repo_name: node-red/node-red
description: Low-code programming for event-driven applications
repo_language: JavaScript
owner: node-red
file_changes: filename: Project.js
change_type: ModificationType.MODIFY
diff: @@ -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();
 }
 

programming_language: JavaScript
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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() {

programming_language: JavaScript