CVE-2020-15152 (published 2020-08-17T22:15Z)

ftp-srv is an npm package which is a modern and extensible FTP server designed to be simple yet configurable. In ftp-srv before versions 2.19.6, 3.1.2, and 4.3.4 are vulnerable to Server-Side Request Forgery. The PORT command allows arbitrary IPs which can be used to cause the server to make a connection elsewhere. A possible workaround is blocking the PORT through the configuration. This issue is fixed in version2 2.19.6, 3.1.2, and 4.3.4. More information can be found on the linked advisory.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 9.1, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/autovance/ftp-srv
commit_details: repo_url: https://github.com/autovance/ftp-srv
author: Tyler Stewart
author_date: 2020-08-17 12:48:21-06:00
msg: fix: disallow PORT connections to alternate hosts

Ensure the data socket that the server connects to from the PORT command is the same IP as the current command socket.

* fix: add error handling to additional connection commands
repository: repo_url: https://github.com/autovance/ftp-srv
repo_name: QuorumDMS/ftp-srv
description: 📮 Modern FTP Server
repo_language: JavaScript
owner: autovance
file_changes: filename: eprt.js
change_type: ModificationType.MODIFY
diff: @@ -8,14 +8,18 @@ const FAMILY = {
 
 module.exports = {
   directive: 'EPRT',
-  handler: function ({command} = {}) {
+  handler: function ({log, command} = {}) {
     const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
     const family = FAMILY[protocol];
     if (!family) return this.reply(504, 'Unknown network protocol');
 
     this.connector = new ActiveConnector(this);
     return this.connector.setupConnection(ip, port, family)
-    .then(() => this.reply(200));
+    .then(() => this.reply(200))
+    .catch((err) => {
+      log.error(err);
+      return this.reply(err.code || 425, err.message);
+    });
   },
   syntax: '{{cmd}} |<protocol>|<address>|<port>|',
   description: 'Specifies an address and port to which the server should connect'

programming_language: JavaScript
filename: epsv.js
change_type: ModificationType.MODIFY
diff: @@ -2,13 +2,17 @@ const PassiveConnector = require('../../connector/passive');
 
 module.exports = {
   directive: 'EPSV',
-  handler: function () {
+  handler: function ({log}) {
     this.connector = new PassiveConnector(this);
     return this.connector.setupServer()
     .then((server) => {
       const {port} = server.address();
 
       return this.reply(229, `EPSV OK (|||${port}|)`);
+    })
+    .catch((err) => {
+      log.error(err);
+      return this.reply(err.code || 425, err.message);
     });
   },
   syntax: '{{cmd}} [<protocol>]',

programming_language: JavaScript
filename: pasv.js
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ module.exports = {
     })
     .catch((err) => {
       log.error(err);
-      return this.reply(425);
+      return this.reply(err.code || 425, err.message);
     });
   },
   syntax: '{{cmd}}',

programming_language: TypeScript
filename: port.js
change_type: ModificationType.MODIFY
diff: @@ -17,7 +17,7 @@ module.exports = {
     .then(() => this.reply(200))
     .catch((err) => {
       log.error(err);
-      return this.reply(425);
+      return this.reply(err.code || 425, err.message);
     });
   },
   syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',

programming_language: JavaScript
filename: active.js
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,9 @@
 const {Socket} = require('net');
 const tls = require('tls');
+const ip = require('ip');
 const Promise = require('bluebird');
 const Connector = require('./base');
+const {SocketError} = require('../errors');
 
 class Active extends Connector {
   constructor(connection) {
@@ -27,6 +29,10 @@ class Active extends Connector {
 
     return closeExistingServer()
     .then(() => {
+      if (!ip.isEqual(this.connection.commandSocket.remoteAddress, host)) {
+        throw new SocketError('The given address is not yours', 500);
+      }
+
       this.dataSocket = new Socket();
       this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
       this.dataSocket.connect({host, port, family}, () => {

programming_language: JavaScript
filename: base.js
change_type: ModificationType.MODIFY
diff: @@ -29,7 +29,7 @@ class Connector {
   closeSocket() {
     if (this.dataSocket) {
       const socket = this.dataSocket;
-      this.dataSocket.end(() => socket.destroy());
+      this.dataSocket.end(() => socket && socket.destroy());
       this.dataSocket = null;
     }
   }

programming_language: TypeScript
filename: eprt.spec.js
change_type: ModificationType.MODIFY
diff: @@ -23,7 +23,7 @@ describe(CMD, function () {
   });
 
   it('// unsuccessful | no argument', () => {
-    return cmdFn()
+    return cmdFn({})
     .then(() => {
       expect(mockClient.reply.args[0][0]).to.equal(504);
     });

programming_language: JavaScript
filename: epsv.spec.js
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ describe(CMD, function () {
   });
 
   it('// successful IPv4', () => {
-    return cmdFn()
+    return cmdFn({})
     .then(() => {
       const [code, message] = mockClient.reply.args[0];
       expect(code).to.equal(229);

programming_language: JavaScript
filename: opts.spec.js
change_type: ModificationType.MODIFY
diff: @@ -29,7 +29,7 @@ describe(CMD, function () {
   it('BAD // unsuccessful', () => {
     return cmdFn({command: {arg: 'BAD', directive: CMD}})
     .then(() => {
-      expect(mockClient.reply.args[0][0]).to.equal(500);
+      expect(mockClient.reply.args[0][0]).to.equal(501);
     });
   });
 

programming_language: JavaScript
filename: active.spec.js
change_type: ModificationType.MODIFY
diff: @@ -13,14 +13,16 @@ describe('Connector - Active //', function () {
   let getNextPort = getNextPortFactory(host, 1024);
   let PORT;
   let active;
-  let mockConnection = {};
+  let mockConnection = {
+    commandSocket: {
+      remoteAddress: '::ffff:127.0.0.1'
+    }
+  };
   let sandbox;
   let server;
 
-  before(() => {
-    active = new ActiveConnector(mockConnection);
-  });
   beforeEach((done) => {
+    active = new ActiveConnector(mockConnection);
     sandbox = sinon.sandbox.create().usingPromise(Promise);
 
     getNextPort()
@@ -31,9 +33,12 @@ describe('Connector - Active //', function () {
       .listen(PORT, () => done());
     });
   });
-  afterEach((done) => {
+
+  afterEach(() => {
     sandbox.restore();
-    server.close(done);
+    server.close();
+    active.end();
+
   });
 
   it('sets up a connection', function () {
@@ -43,13 +48,27 @@ describe('Connector - Active //', function () {
     });
   });
 
-  it('destroys existing connection, then sets up a connection', function () {
-    const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
+  it('rejects alternative host', function () {
+    return active.setupConnection('123.45.67.89', PORT)
+    .catch((err) => {
+      expect(err.code).to.equal(500);
+      expect(err.message).to.equal('The given address is not yours');
+    })
+    .finally(() => {
+      expect(active.dataSocket).not.to.exist;
+    });
+  });
 
+  it('destroys existing connection, then sets up a connection', function () {
     return active.setupConnection(host, PORT)
     .then(() => {
-      expect(destroyFnSpy.callCount).to.equal(1);
-      expect(active.dataSocket).to.exist;
+      const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
+
+      return active.setupConnection(host, PORT)
+      .then(() => {
+        expect(destroyFnSpy.callCount).to.equal(1);
+        expect(active.dataSocket).to.exist;
+      });
     });
   });
 

programming_language: JavaScript