Path traversal in Node-Red
Description
Node-RED 1.2.7 and earlier has an arbitrary path traversal vulnerability in the Projects API, allowing users with projects.read permission to access any file on the system.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Node-RED 1.2.7 and earlier has an arbitrary path traversal vulnerability in the Projects API, allowing users with `projects.read` permission to access any file on the system.
Vulnerability
Analysis
CVE-2021-21298 is a path traversal vulnerability in the Node-RED Projects API, affecting versions 1.2.7 and earlier. The root cause lies in insufficient validation of file paths provided by users when interacting with project files through the API. Specifically, the getFile, update, and related methods did not check if the requested path stayed within the project directory, allowing traversal via relative paths like ../ [2].
Exploitation
Prerequisites
This vulnerability is only exploitable when the Projects feature is enabled (it is not enabled by default). An attacker must also have a valid account with at least projects.read permission in the Node-RED editor [1][3]. With these prerequisites, an attacker can craft API calls that escape the intended project directory and read arbitrary files from the server's filesystem.
Impact
A successful exploit allows an attacker with projects.read permission to read any file the Node-RED process can access on the host system. This includes sensitive files such as configuration files, credentials, and other application data, leading to potential information disclosure and further compromise [1].
Mitigation
The vulnerability has been patched in Node-RED version 1.2.8 [4]. The fix adds path traversal checks using fspath.relative() to reject any file path that resolves outside the project directory [2]. As a workaround, administrators should not grant untrusted users read access to the Node-RED editor [3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@node-red/runtimenpm | < 1.2.8 | 1.2.8 |
Affected products
2Patches
174db3e17d075Restrict project file access to inside the project directory
2 files changed · +36 −3
packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js+17 −2 modified@@ -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() {
packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/Project.js+19 −1 modified@@ -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(); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-m33v-338h-4v9fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-21298ghsaADVISORY
- github.com/node-red/node-red/commit/74db3e17d075f23d9c95d7871586cf461524c456ghsax_refsource_MISCWEB
- github.com/node-red/node-red/releases/tag/1.2.8ghsax_refsource_MISCWEB
- github.com/node-red/node-red/security/advisories/GHSA-m33v-338h-4v9fghsax_refsource_CONFIRMWEB
- www.npmjs.com/package/%40node-red/runtimemitrex_refsource_MISC
- www.npmjs.com/package/@node-red/runtimeghsaWEB
News mentions
0No linked articles in our index yet.