VYPR
Critical severityNVD Advisory· Published Aug 17, 2020· Updated Aug 4, 2024

Server-Side Request Forgery in ftp-srv

CVE-2020-15152

Description

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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An SSRF vulnerability in ftp-srv allows unauthenticated attackers to abuse the PORT command to make server connections to arbitrary IPs.

CVE-2020-15152 is a Server-Side Request Forgery (SSRF) vulnerability in the ftp-srv npm package, a configurable FTP server. The root cause lies in the handling of the PORT command, which accepts arbitrary IP addresses and ports without validation. When an active data connection is requested via the PORT command, the server attempts to connect to the specified address, enabling SSRF.[1]

An unauthenticated attacker can send a crafted PORT command to the FTP server, specifying an arbitrary IP and port. This forces the server to initiate a TCP connection to the attacker's target. No authentication is required, and the attacker does not need prior access to the server. The attack surface is the exposed FTP control channel.[1]

The impact is that an attacker can use the FTP server as a proxy to scan internal networks or interact with internal services that would otherwise be inaccessible. This can lead to reconnaissance, service enumeration, or further exploitation of internal resources. No code execution is directly achieved, but the SSRF serves as a stepping stone for deeper compromise.[1]

The issue is fixed in versions 2.19.6, 3.1.2, and 4.3.4. The fixes introduce validation that restricts PORT connections to the client's own IP address, and also improve error handling.[2][3][4] As a workaround, users can disable the PORT command via configuration if upgrading is not immediately possible.[1]

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.

PackageAffected versionsPatched versions
ftp-srvnpm
>= 1.0.0, < 2.19.62.19.6
ftp-srvnpm
>= 3.0.0, < 3.1.23.1.2
ftp-srvnpm
>= 4.0.0, < 4.3.44.3.4

Affected products

3

Patches

3
fb32b012c3ba

fix: disallow PORT connections to alternate hosts

https://github.com/autovance/ftp-srvTyler StewartAug 17, 2020via ghsa
10 files changed · +60 19
  • src/commands/registration/eprt.js+6 2 modified
    @@ -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'
    
  • src/commands/registration/epsv.js+5 1 modified
    @@ -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>]',
    
  • src/commands/registration/pasv.js+4 0 modified
    @@ -13,6 +13,10 @@ module.exports = {
           const portByte2 = port % 256;
     
           return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
    +    })
    +    .catch((err) => {
    +      log.error(err);
    +      return this.reply(err.code || 425, err.message);
         });
       },
       syntax: '{{cmd}}',
    
  • src/commands/registration/port.js+5 1 modified
    @@ -14,7 +14,11 @@ module.exports = {
         const port = portBytes[0] * 256 + portBytes[1];
     
         return this.connector.setupConnection(ip, port)
    -    .then(() => this.reply(200));
    +    .then(() => this.reply(200))
    +    .catch((err) => {
    +      log.error(err);
    +      return this.reply(err.code || 425, err.message);
    +    });
       },
       syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
       description: 'Specifies an address and port to which the server should connect'
    
  • src/connector/active.js+6 0 modified
    @@ -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.setEncoding(this.connection.transferType);
           this.dataSocket.on('error', err => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
    
  • src/connector/base.js+1 1 modified
    @@ -28,7 +28,7 @@ class Connector {
     
       end() {
         const closeDataSocket = new Promise(resolve => {
    -      if (this.dataSocket) this.dataSocket.end();
    +      if (this.dataSocket) this.dataSocket.end(() => socket && socket.destroy());
           else resolve();
         });
         const closeDataServer = new Promise(resolve => {
    
  • test/commands/registration/eprt.spec.js+1 1 modified
    @@ -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);
         });
    
  • test/commands/registration/epsv.spec.js+1 1 modified
    @@ -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);
    
  • test/commands/registration/opts.spec.js+1 1 modified
    @@ -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);
         });
       });
     
    
  • test/connector/active.spec.js+30 11 modified
    @@ -1,6 +1,7 @@
     /* eslint no-unused-expressions: 0 */
     const {expect} = require('chai');
     const sinon = require('sinon');
    +const Promise = require('bluebird');
     
     const net = require('net');
     const tls = require('tls');
    @@ -11,15 +12,17 @@ const findPort = require('../../src/helpers/find-port');
     describe('Connector - Active //', function () {
       let PORT;
       let active;
    -  let mockConnection = {};
    +  let mockConnection = {
    +    commandSocket: {
    +      remoteAddress: '::ffff:127.0.0.1'
    +    }
    +  };
       let sandbox;
       let server;
     
    -  before(() => {
    +  beforeEach((done) => {
         active = new ActiveConnector(mockConnection);
    -  });
    -  beforeEach(done => {
    -    sandbox = sinon.sandbox.create();
    +    sandbox = sinon.sandbox.create().usingPromise(Promise);
     
         findPort()
         .then(port => {
    @@ -29,9 +32,11 @@ 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 () {
    @@ -41,13 +46,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('127.0.0.1', PORT)
         .then(() => {
    -      expect(destroyFnSpy.callCount).to.equal(1);
    -      expect(active.dataSocket).to.exist;
    +      const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
    +
    +      return active.setupConnection('127.0.0.1', PORT)
    +      .then(() => {
    +        expect(destroyFnSpy.callCount).to.equal(1);
    +        expect(active.dataSocket).to.exist;
    +      });
         });
       });
     
    
5508c2346cf2

fix: disallow PORT connections to alternate hosts

https://github.com/autovance/ftp-srvTyler StewartAug 17, 2020via ghsa
10 files changed · +60 19
  • src/commands/registration/eprt.js+6 2 modified
    @@ -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'
    
  • src/commands/registration/epsv.js+5 1 modified
    @@ -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>]',
    
  • src/commands/registration/pasv.js+4 0 modified
    @@ -13,6 +13,10 @@ module.exports = {
           const portByte2 = port % 256;
     
           return this.reply(227, `PASV OK (${host},${portByte1},${portByte2})`);
    +    })
    +    .catch((err) => {
    +      log.error(err);
    +      return this.reply(err.code || 425, err.message);
         });
       },
       syntax: '{{cmd}}',
    
  • src/commands/registration/port.js+5 1 modified
    @@ -14,7 +14,11 @@ module.exports = {
         const port = portBytes[0] * 256 + portBytes[1];
     
         return this.connector.setupConnection(ip, port)
    -    .then(() => this.reply(200));
    +    .then(() => this.reply(200))
    +    .catch((err) => {
    +      log.error(err);
    +      return this.reply(err.code || 425, err.message);
    +    });
       },
       syntax: '{{cmd}} <x>,<x>,<x>,<x>,<y>,<y>',
       description: 'Specifies an address and port to which the server should connect'
    
  • src/connector/active.js+6 0 modified
    @@ -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.setEncoding(this.connection.transferType);
           this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
    
  • src/connector/base.js+1 1 modified
    @@ -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;
         }
       }
    
  • test/commands/registration/eprt.spec.js+1 1 modified
    @@ -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);
         });
    
  • test/commands/registration/epsv.spec.js+1 1 modified
    @@ -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);
    
  • test/commands/registration/opts.spec.js+1 1 modified
    @@ -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);
         });
       });
     
    
  • test/connector/active.spec.js+30 11 modified
    @@ -12,14 +12,16 @@ describe('Connector - Active //', function () {
       let getNextPort = getNextPortFactory(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()
    @@ -30,9 +32,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 () {
    @@ -42,13 +47,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;
    +    });
    +  });
     
    -    return active.setupConnection('127.0.0.1', PORT)
    +  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;
    +      });
         });
       });
     
    
e449e75219d9

fix: disallow PORT connections to alternate hosts

https://github.com/autovance/ftp-srvTyler StewartAug 17, 2020via ghsa
10 files changed · +52 19
  • src/commands/registration/eprt.js+6 2 modified
    @@ -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'
    
  • src/commands/registration/epsv.js+5 1 modified
    @@ -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>]',
    
  • src/commands/registration/pasv.js+1 1 modified
    @@ -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}}',
    
  • src/commands/registration/port.js+1 1 modified
    @@ -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>',
    
  • src/connector/active.js+6 0 modified
    @@ -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}, () => {
    
  • src/connector/base.js+1 1 modified
    @@ -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;
         }
       }
    
  • test/commands/registration/eprt.spec.js+1 1 modified
    @@ -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);
         });
    
  • test/commands/registration/epsv.spec.js+1 1 modified
    @@ -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);
    
  • test/commands/registration/opts.spec.js+1 1 modified
    @@ -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);
         });
       });
     
    
  • test/connector/active.spec.js+29 10 modified
    @@ -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;
    +      });
         });
       });
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.