From 322cf37916b1b2729914053b8a7d1f213f75549a Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 01:25:32 -0800 Subject: [PATCH 01/12] Confine MDTM request to files in CHROOT jail This commit properly resolves MDTM request filenames to locations within the CHROOT jail. Previously, requests were made relative to the filesystem root (/), instead of the server root (/srv/files/from/here). This allowed users to request MDTM on potentially sensitive files (/root, /home), while simultaneously denying legitimate requests within the shared directory. Note: all filesystem calls *must* be joined with the path to the server root (pathModule.join(self.root, filename)). fix https://github.com/asylumfunk/nodeftpd/issues/28 ref https://github.com/sstur/nodeftpd/commit/57a9e5f9e0c9fd3e5fbdd7f847d5f0116dbd5a4a ref https://github.com/sstur/nodeftpd/pull/5 ref https://github.com/sstur/nodeftpd/pull/9 --- lib/ftpd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 9acd401..3c58dbb 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -548,7 +548,7 @@ FtpConnection.prototype._command_MDTM = function (commandArg) { var file = withCwd(self.cwd, commandArg); - self.fs.stat(file, function (err, stats) { + self.fs.stat(pathModule.join(self.root, file), function (err, stats) { if (err) { if (err.code == 'ENOENT') self.respond("550 Not found"); From 24fdbfd175b5795bd5656289746f3ce97ffb937b Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 01:46:05 -0800 Subject: [PATCH 02/12] Surround all blocks with curly braces Style aside, this reduces bugs. In this case, the return statement was indented such that it appeared to be part of the else block, whereas it was actually executed regardless. This makes the code more robust. --- lib/ftpd.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 3c58dbb..dcf9001 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -550,14 +550,14 @@ FtpConnection.prototype._command_MDTM = function (commandArg) { self.fs.stat(pathModule.join(self.root, file), function (err, stats) { if (err) { - if (err.code == 'ENOENT') + if (err.code == 'ENOENT') { self.respond("550 Not found"); - else + } else { self.respond("550 Error attempting to determine modification time"); - return; } - + } else { self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss") + ""); + } }); }; From 3122263baff8e6ef1b279362b56ea375d0189dcb Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 01:53:03 -0800 Subject: [PATCH 03/12] Remove superfluous empty string This string previously contained a sequence, when commands were still writing directly to the socket. ref https://github.com/sstur/nodeftpd/pull/2 ref 22c8d1754d84b4ed1242a43ef89945ee4d95eb4f --- lib/ftpd.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index dcf9001..4a0d9d2 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -556,7 +556,7 @@ FtpConnection.prototype._command_MDTM = function (commandArg) { self.respond("550 Error attempting to determine modification time"); } } else { - self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss") + ""); + self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); } }); }; From 29479fe4a64fe4e8613cfd20e777fa2804dfa44a Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 11:23:01 -0800 Subject: [PATCH 04/12] Remove superfluous newline --- lib/ftpd.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 4a0d9d2..a1c0a58 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -545,7 +545,6 @@ FtpConnection.prototype._command_FEAT = function(commandArg) { FtpConnection.prototype._command_MDTM = function (commandArg) { var self = this; - var file = withCwd(self.cwd, commandArg); self.fs.stat(pathModule.join(self.root, file), function (err, stats) { From b0bd8fb9354cfa865744c26b4b0333a72386630d Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 11:26:03 -0800 Subject: [PATCH 05/12] Return "this" instead of "undefined" Methods without a return statement (including an empty statement) implicitly return "undefined", limiting their usefullness. By returning "this" from any object method without another return value, we allow method calls to be chained together. --- lib/ftpd.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ftpd.js b/lib/ftpd.js index a1c0a58..8f1cf88 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -558,6 +558,8 @@ FtpConnection.prototype._command_MDTM = function (commandArg) { self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); } }); + + return this; }; FtpConnection.prototype._command_LIST = function(commandArg) { From cffdc62a36ff61be69fe6bca60160e6da7a38061 Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 11:20:17 -0800 Subject: [PATCH 06/12] Remove superfluous response In this case, if we can't find or access a file, the distinction is irrelevant. The generic error message would hint that the file actually does exist, but is inaccessible for some other reason (insufficient permissions, etc.). This behavior could have been leveraged by a malicious user and should be removed. --- lib/ftpd.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 8f1cf88..019813c 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -549,11 +549,7 @@ FtpConnection.prototype._command_MDTM = function (commandArg) { self.fs.stat(pathModule.join(self.root, file), function (err, stats) { if (err) { - if (err.code == 'ENOENT') { - self.respond("550 Not found"); - } else { - self.respond("550 Error attempting to determine modification time"); - } + self.respond("550 File unavailable"); } else { self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); } From f0ec043d39ba46fee5123b681ed9dd11e48645ee Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 12:23:06 -0800 Subject: [PATCH 07/12] Use a meaningful parameter name Resolve file path more clearly --- lib/ftpd.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 019813c..afd8dad 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -543,11 +543,12 @@ FtpConnection.prototype._command_FEAT = function(commandArg) { ); }; -FtpConnection.prototype._command_MDTM = function (commandArg) { +FtpConnection.prototype._command_MDTM = function (file) { var self = this; - var file = withCwd(self.cwd, commandArg); + file = withCwd(self.cwd, file); + file = pathModule.join(self.root, file); - self.fs.stat(pathModule.join(self.root, file), function (err, stats) { + self.fs.stat(file, function (err, stats) { if (err) { self.respond("550 File unavailable"); } else { From 1fb8f0475e6c22ff66fa26b2cfd85e709b0f645f Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 12:32:27 -0800 Subject: [PATCH 08/12] Remove "self" variable Previously, "self" was being used to maintain a pointer to the "this" value outside of the anonymous function. Instead, we use the native Function.bind(...) method to bind the method to the caller's namespace. Beyond removing a superfluous variable, this also prevents a user from mistakenly referencing "this" inside the anonymous function instead of "self". --- lib/ftpd.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index afd8dad..a7a8d89 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -544,17 +544,16 @@ FtpConnection.prototype._command_FEAT = function(commandArg) { }; FtpConnection.prototype._command_MDTM = function (file) { - var self = this; - file = withCwd(self.cwd, file); - file = pathModule.join(self.root, file); + file = withCwd(this.cwd, file); + file = pathModule.join(this.root, file); - self.fs.stat(file, function (err, stats) { + this.fs.stat(file, function (err, stats) { if (err) { - self.respond("550 File unavailable"); + this.respond("550 File unavailable"); } else { - self.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); + this.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); } - }); + }.bind(this)); return this; }; From 928fb66e70eed0ba9fb0a7b0c58c392b91b9776c Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 12:28:24 -0800 Subject: [PATCH 09/12] Add jsdoc for MDTM command --- lib/ftpd.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/ftpd.js b/lib/ftpd.js index a7a8d89..53eae00 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -543,6 +543,11 @@ FtpConnection.prototype._command_FEAT = function(commandArg) { ); }; +/** + * Print the file modification time + * @param {string} file + * @return {FtpConnection} this + */ FtpConnection.prototype._command_MDTM = function (file) { file = withCwd(this.cwd, file); file = pathModule.join(this.root, file); From 39828abfd7c72e572fc4eb1ad0f35edaa0dce9f1 Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 13:52:49 -0800 Subject: [PATCH 10/12] Add test coverage for MDTM This commit tests all branches of the MDTM command. It should accept requests for existing files and reject requests for invalid files. --- test/mdtm.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/mdtm.js diff --git a/test/mdtm.js b/test/mdtm.js new file mode 100644 index 0000000..04f0d02 --- /dev/null +++ b/test/mdtm.js @@ -0,0 +1,33 @@ +var common = require('./common'); + +describe('MDTM command', function () { + 'use strict'; + + var client; + var server; + + beforeEach(function (done) { + server = common.server(); + client = common.client(done); + }); + + it('should respond 213 for a valid file', function (done) { + client.raw('MDTM', '/data.txt', function (error, response) { + common.should.not.exist(error); + response.text.should.match(/^213 [0-9]{14}$/); + done(); + }); + }); + + it('should respond 550 for an invalid file', function (done) { + client.raw('MDTM', '/data2.txt', function (error, response) { + error.code.should.equal(550); + done(); + }); + }); + + afterEach(function () { + server.close(); + }); +}); + From 9b9771a5f2354547dc0b40ff3499c836d0511d93 Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 14:14:19 -0800 Subject: [PATCH 11/12] Remove superfluous newlines --- lib/ftpd.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/ftpd.js b/lib/ftpd.js index 53eae00..fc8ea57 100644 --- a/lib/ftpd.js +++ b/lib/ftpd.js @@ -551,7 +551,6 @@ FtpConnection.prototype._command_FEAT = function(commandArg) { FtpConnection.prototype._command_MDTM = function (file) { file = withCwd(this.cwd, file); file = pathModule.join(this.root, file); - this.fs.stat(file, function (err, stats) { if (err) { this.respond("550 File unavailable"); @@ -559,7 +558,6 @@ FtpConnection.prototype._command_MDTM = function (file) { this.respond("213 " + dateformat(stats.mtime, "yyyymmddhhMMss")); } }.bind(this)); - return this; }; From 65ca58e12f57b6ee7efd0106b896f05777dfac8d Mon Sep 17 00:00:00 2001 From: asylumfunk Date: Fri, 7 Mar 2014 14:37:29 -0800 Subject: [PATCH 12/12] Bump version to 0.2.7 Following the bug fixed at 322cf37916b1b2729914053b8a7d1f213f75549a, it's time to push an update. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3237191..511124f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ftpd", - "version": "0.2.6", + "version": "0.2.7", "description": "Node FTP Server", "main": "./lib/ftpd.js", "scripts": {