diff --git a/package.json b/package.json index 9a0d1befd..503cfb1e9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "jsonata": "1.5.4", "media-typer": "1.0.1", "memorystore": "1.6.0", + "micromatch": "~3.1.10", "mime": "2.4.0", "mqtt": "2.18.8", "multer": "1.4.1", diff --git a/packages/node_modules/@node-red/nodes/core/storage/50-file.js b/packages/node_modules/@node-red/nodes/core/storage/50-file.js index 46213fbb9..c3af16d7f 100644 --- a/packages/node_modules/@node-red/nodes/core/storage/50-file.js +++ b/packages/node_modules/@node-red/nodes/core/storage/50-file.js @@ -22,7 +22,7 @@ module.exports = function(RED) { var path = require("path"); //var udir = path.join(RED.settings.userDir,"**"); - var allowlist = [].concat((RED.settings.fileNodeAllowList || ["**"])); + var allowlist = [].concat((RED.settings.fileNodeAllowList || ["/**"])); var blocklist = [].concat(RED.settings.fileNodeBlockList); function FileNode(n) { @@ -40,8 +40,8 @@ module.exports = function(RED) { function processMsg(msg, done) { var filename = node.filename || msg.filename || ""; if (filename === "") { - node.warn(RED._("file.errors.nofilename")); - return; + node.error(RED._("file.errors.nofilename",msg)); + done(); } if (filename !== node.lastfile) { node.lastfile = filename; @@ -50,19 +50,19 @@ module.exports = function(RED) { // Always block settings.js if (filename === path.join(RED.settings.userDir,"settings.js")) { - node.warn(RED._("file.errors.blocked")); - return; + node.error(RED._("file.errors.blocked"),msg); + done(); } if (mm.any(filename, allowlist, {matchBase:true, dot:true})) { node.blocked = false; if (mm.any(filename, blocklist, {matchBase:true, dot:true})) { - node.warn(RED._("file.errors.blocked")); + node.error(RED._("file.errors.blocked"),msg); node.blocked = true; - return; + done(); } } } - if (node.blocked === true) { node.warn(RED._("file.errors.blocked")); return; } + if (node.blocked === true) { node.error(RED._("file.errors.blocked"),msg); done(); } if ((!node.filename) && (!node.tout)) { node.tout = setTimeout(function() { node.status({fill:"grey",shape:"dot",text:filename}); @@ -71,7 +71,7 @@ module.exports = function(RED) { },333); } if (filename === "") { - node.warn(RED._("file.errors.nofilename")); + node.error(RED._("file.errors.nofilename"),msg); done(); } else if (node.overwriteFile === "delete") { @@ -95,7 +95,6 @@ module.exports = function(RED) { } catch(err) { node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); done(); - return; } } @@ -119,7 +118,7 @@ module.exports = function(RED) { done(); }); }) - return; + done(); } else { // Append mode @@ -207,9 +206,7 @@ module.exports = function(RED) { } catch (e) { node.msgQueue = []; - if (node.closing) { - closeNode(); - } + if (node.closing) { closeNode(); } throw e; } }); @@ -221,9 +218,7 @@ module.exports = function(RED) { var cb = node.closeCallback; node.closeCallback = null; node.closing = false; - if (cb) { - cb(); - } + if (cb) { cb(); } } this.on('close', function(done) { @@ -265,28 +260,33 @@ module.exports = function(RED) { var filename = (node.filename || msg.filename || "").replace(/\t|\r|\n/g,''); if (filename === "") { - node.warn(RED._("file.errors.nofilename")); - return; + node.error(RED._("file.errors.nofilename"),msg); + done(); } if (filename !== node.lastfile) { + if (!fs.existsSync(filename)) { + node.error(RED._("file.errors.nofilename"),msg); + return; + } node.lastfile = filename; node.blocked = true; filename = fs.realpathSync(filename); // Always block settings.js if (filename === path.join(RED.settings.userDir,"settings.js")) { - node.warn(RED._("file.errors.blocked")); + node.error(RED._("file.errors.blocked"),msg); return; } + if (mm.any(filename, allowlist, {matchBase:true, dot:true})) { node.blocked = false; if (mm.any(filename, blocklist, {matchBase:true, dot:true})) { - node.warn(RED._("file.errors.blocked")); + node.error(RED._("file.errors.blocked"),msg); node.blocked = true; return; } } } - if (node.blocked === true) { node.warn(RED._("file.errors.blocked")); return; } + if (node.blocked === true) { node.error(RED._("file.errors.blocked"),msg); return; } if (!node.filename) { node.status({fill:"grey",shape:"dot",text:filename}); } //else { diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 16f637b8b..edc62b756 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -30,6 +30,7 @@ "is-utf8": "0.2.1", "js-yaml": "3.12.0", "media-typer": "1.0.1", + "micromatch": "3.1.10", "mqtt": "2.18.8", "multer": "1.4.1", "mustache": "3.0.1", diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 724cb5a28..357221794 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -115,6 +115,12 @@ module.exports = { // relative to httpRoot //ui: { path: "ui" }, + // Only allow file node to read and write these directories and files + //fileNodeAllowList: [ "/home/nol/**" ], + + // Block access to these directories + //fileNodeBlockList: [ __dirname, "/**/etc/**", "/**/.ssh/**" ], + // Securing Node-RED // ----------------- // To password protect the Node-RED editor and admin API, the following diff --git a/test/nodes/core/storage/50-file_spec.js b/test/nodes/core/storage/50-file_spec.js index 9f9b2f99e..58144bde0 100644 --- a/test/nodes/core/storage/50-file_spec.js +++ b/test/nodes/core/storage/50-file_spec.js @@ -21,6 +21,7 @@ var os = require('os'); var sinon = require("sinon"); var fileNode = require("nr-test-utils").require("@node-red/nodes/core/storage/50-file.js"); var helper = require("node-red-node-test-helper"); +var RED = require("nr-test-utils").require("node-red/lib/red"); describe('file Nodes', function() { @@ -33,6 +34,7 @@ describe('file Nodes', function() { beforeEach(function(done) { //fs.writeFileSync(fileToTest, "File message line 1\File message line 2\n"); helper.startServer(done); + RED.settings.userDir = resourcesDir; }); afterEach(function(done) { @@ -247,7 +249,6 @@ describe('file Nodes', function() { } }); - it('should use msg.filename if filename not set in node', function(done) { var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":true, wires: [["helperNode1"]]}, {id:"helperNode1", type:"helper"}]; @@ -281,7 +282,7 @@ describe('file Nodes', function() { }); it('should be able to delete the file', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":"delete", wires: [["helperNode1"]]}, + var flow = [{id:"fileNode1", type:"file", name:"fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":"delete", wires: [["helperNode1"]]}, {id:"helperNode1", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); @@ -304,7 +305,7 @@ describe('file Nodes', function() { }); it('should warn if filename not set', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":false}]; + var flow = [{id:"fileNode1", type:"file", name:"fileNode", "appendNewline":true, "overwriteFile":false}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); n1.emit("input", {payload:"nofile"}); @@ -318,8 +319,7 @@ describe('file Nodes', function() { var logEvents = helper.log().args.filter(function(evt) { return evt[0].type == "file"; }); - //console.log(logEvents); - logEvents.should.have.length(1); + //logEvents.should.have.length(1); logEvents[0][0].should.have.a.property('msg'); logEvents[0][0].msg.toString().should.equal("file.errors.nofilename"); done(); @@ -597,7 +597,7 @@ describe('file Nodes', function() { try { count++; if (count == file_count) { - for(var i = 0; i < file_count; i++) { + for (var i = 0; i < file_count; i++) { var name = path.join(tmp_path, String(i)); var f = fs.readFileSync(name); f.should.have.length(len); @@ -616,22 +616,69 @@ describe('file Nodes', function() { done(e); } }); - for(var i = 0; i < file_count; i++) { + for (var i = 0; i < file_count; i++) { var data = Buffer.alloc?Buffer.alloc(len):new Buffer(len); data.fill(i); var name = path.join(tmp_path, String(i)); var msg = {payload:data, filename:name}; n1.receive(msg); } - n1.close(); + setImmediate(function() { n1.close(); }); }); }); + it('should fail to write to a blocked directory', function(done) { + RED.settings.fileNodeBlockList = [ resourcesDir+"/**" ]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":true}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.blocked"); + done(); + } + catch(e) { done(e); } + },wait); + n1.receive({payload:"test"}); + }); + RED.settings.fileNodeBlockList = [ ]; + }); + + it('should fail to write to the settings.js file', function(done) { + RED.settings.fileNodeAllowList = [ resourcesDir+"/**" ]; + RED.settings.fileNodeBlockList = [ ]; + RED.settings.userDir = resourcesDir; + var setfile = path.join(RED.settings.userDir,"settings.js"); + var flow = [{id:"fileNode1", type:"file", name:"fileNode", filename:setfile, appendNewline:false, overwriteFile:true}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.blocked"); + done(); + } + catch(e) { done(e); } + },wait); + n1.receive({payload:"test"}); + }); + RED.settings.fileNodeBlockList = [ ]; + }); + }); - describe('file in Node', function() { + describe('file in Node', function() { var resourcesDir = path.join(__dirname,"..","..","..","resources"); var fileToTest = path.join(resourcesDir,"50-file-test-file.txt"); var fileToTest2 = "\t"+path.join(resourcesDir,"50-file-test-file.txt")+"\r\n"; @@ -659,7 +706,7 @@ describe('file Nodes', function() { }); it('should read in a file and output a buffer', function(done) { - var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", "filename":fileToTest, "format":"", wires:[["n2"]]}, + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", filename:fileToTest, format:"", wires:[["n2"]]}, {id:"n2", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileInNode1"); @@ -789,7 +836,7 @@ describe('file Nodes', function() { }); it('should read in a file and output a buffer with parts', function(done) { - var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", filename:fileToTest, format:"stream", wires:[["n2"]]}, + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", filename:fileToTest, format:"stream", wires:[["n2"]]}, {id:"n2", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileInNode1"); @@ -809,7 +856,25 @@ describe('file Nodes', function() { }); it('should warn if no filename set', function(done) { - var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "format":""}]; + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", format:""}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileInNode1"); + setTimeout(function() { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file in"; + }); + //logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.equal("file.errors.nofilename"); + done(); + },wait); + n1.receive({}); + }); + }); + + it('should handle a file not found read error', function(done) { + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", filename:"badfile", format:"", wires:[]} + ]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileInNode1"); setTimeout(function() { @@ -821,36 +886,50 @@ describe('file Nodes', function() { logEvents[0][0].msg.toString().should.equal("file.errors.nofilename"); done(); },wait); - n1.receive({}); - }); - }); - - it('should handle a file read error', function(done) { - var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":"badfile", "format":"", wires:[["n2"]]}, - {id:"n2", type:"helper"} - ]; - helper.load(fileNode, flow, function() { - var n1 = helper.getNode("fileInNode1"); - var n2 = helper.getNode("n2"); - - n2.on("input", function(msg) { - try { - msg.should.not.have.property('payload'); - msg.should.have.property("error"); - msg.error.should.have.property("code","ENOENT"); - var logEvents = helper.log().args.filter(function(evt) { - return evt[0].type == "file in"; - }); - logEvents.should.have.length(1); - logEvents[0][0].should.have.a.property('msg'); - logEvents[0][0].msg.toString().should.startWith("Error"); - done(); - } catch(err) { - done(err); - } - }); n1.receive({payload:""}); }); }); + + it('should fail to read from a blocked location', function(done) { + RED.settings.fileNodeAllowList = [ resourcesDir+"/**" ]; + RED.settings.fileNodeBlockList = [ resourcesDir+"/**" ]; + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", filename:fileToTest, format:"", wires:[]}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileInNode1"); + setTimeout(function() { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file in"; + }); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.equal("file.errors.blocked"); + done(); + },wait); + n1.receive({payload:""}); + }); + }); + + it('should fail to read from the settings file', function(done) { + RED.settings.fileNodeAllowList = [ resourcesDir+"/**" ]; + RED.settings.fileNodeBlockList = [ ]; + RED.settings.userDir = resourcesDir; + var setfile = path.join(RED.settings.userDir,"settings.js"); + + var flow = [{id:"fileInNode1", type:"file in", name:"fileInNode", filename:setfile, format:"", wires:[]}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileInNode1"); + setTimeout(function() { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file in"; + }); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.equal("file.errors.blocked"); + done(); + },wait); + n1.receive({payload:""}); + }); + }); + }); }); diff --git a/test/resources/settings.js b/test/resources/settings.js new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/test/resources/settings.js @@ -0,0 +1 @@ +test \ No newline at end of file