Skip to content

Commit

Permalink
fix: disallow PORT connections to alternate hosts
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Tyler Stewart committed Aug 17, 2020
1 parent 66fc66e commit fb32b01
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 19 deletions.
8 changes: 6 additions & 2 deletions src/commands/registration/eprt.js
Expand Up @@ -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'
Expand Down
6 changes: 5 additions & 1 deletion src/commands/registration/epsv.js
Expand Up @@ -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>]',
Expand Down
4 changes: 4 additions & 0 deletions src/commands/registration/pasv.js
Expand Up @@ -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}}',
Expand Down
6 changes: 5 additions & 1 deletion src/commands/registration/port.js
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions src/connector/active.js
@@ -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) {
Expand All @@ -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}));
Expand Down
2 changes: 1 addition & 1 deletion src/connector/base.js
Expand Up @@ -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 => {
Expand Down
2 changes: 1 addition & 1 deletion test/commands/registration/eprt.spec.js
Expand Up @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion test/commands/registration/epsv.spec.js
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/commands/registration/opts.spec.js
Expand Up @@ -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);
});
});

Expand Down
41 changes: 30 additions & 11 deletions test/connector/active.spec.js
@@ -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');
Expand All @@ -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 => {
Expand All @@ -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 () {
Expand All @@ -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;
});
});
});

Expand Down

0 comments on commit fb32b01

Please sign in to comment.