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
This commit is contained in:
Tyler Stewart
2020-08-17 12:48:21 -06:00
parent 296573ae01
commit e449e75219
10 changed files with 52 additions and 19 deletions

View File

@@ -8,14 +8,18 @@ const FAMILY = {
module.exports = { module.exports = {
directive: 'EPRT', directive: 'EPRT',
handler: function ({command} = {}) { handler: function ({log, command} = {}) {
const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value(); const [, protocol, ip, port] = _.chain(command).get('arg', '').split('|').value();
const family = FAMILY[protocol]; const family = FAMILY[protocol];
if (!family) return this.reply(504, 'Unknown network protocol'); if (!family) return this.reply(504, 'Unknown network protocol');
this.connector = new ActiveConnector(this); this.connector = new ActiveConnector(this);
return this.connector.setupConnection(ip, port, family) 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>|', syntax: '{{cmd}} |<protocol>|<address>|<port>|',
description: 'Specifies an address and port to which the server should connect' description: 'Specifies an address and port to which the server should connect'

View File

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

View File

@@ -25,7 +25,7 @@ module.exports = {
}) })
.catch((err) => { .catch((err) => {
log.error(err); log.error(err);
return this.reply(425); return this.reply(err.code || 425, err.message);
}); });
}, },
syntax: '{{cmd}}', syntax: '{{cmd}}',

View File

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

View File

@@ -1,7 +1,9 @@
const {Socket} = require('net'); const {Socket} = require('net');
const tls = require('tls'); const tls = require('tls');
const ip = require('ip');
const Promise = require('bluebird'); const Promise = require('bluebird');
const Connector = require('./base'); const Connector = require('./base');
const {SocketError} = require('../errors');
class Active extends Connector { class Active extends Connector {
constructor(connection) { constructor(connection) {
@@ -27,6 +29,10 @@ class Active extends Connector {
return closeExistingServer() return closeExistingServer()
.then(() => { .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 = new Socket();
this.dataSocket.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err})); 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}, () => { this.dataSocket.connect({host, port, family}, () => {

View File

@@ -29,7 +29,7 @@ class Connector {
closeSocket() { closeSocket() {
if (this.dataSocket) { if (this.dataSocket) {
const socket = this.dataSocket; const socket = this.dataSocket;
this.dataSocket.end(() => socket.destroy()); this.dataSocket.end(() => socket && socket.destroy());
this.dataSocket = null; this.dataSocket = null;
} }
} }

View File

@@ -23,7 +23,7 @@ describe(CMD, function () {
}); });
it('// unsuccessful | no argument', () => { it('// unsuccessful | no argument', () => {
return cmdFn() return cmdFn({})
.then(() => { .then(() => {
expect(mockClient.reply.args[0][0]).to.equal(504); expect(mockClient.reply.args[0][0]).to.equal(504);
}); });

View File

@@ -25,7 +25,7 @@ describe(CMD, function () {
}); });
it('// successful IPv4', () => { it('// successful IPv4', () => {
return cmdFn() return cmdFn({})
.then(() => { .then(() => {
const [code, message] = mockClient.reply.args[0]; const [code, message] = mockClient.reply.args[0];
expect(code).to.equal(229); expect(code).to.equal(229);

View File

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

View File

@@ -13,14 +13,16 @@ describe('Connector - Active //', function () {
let getNextPort = getNextPortFactory(host, 1024); let getNextPort = getNextPortFactory(host, 1024);
let PORT; let PORT;
let active; let active;
let mockConnection = {}; let mockConnection = {
commandSocket: {
remoteAddress: '::ffff:127.0.0.1'
}
};
let sandbox; let sandbox;
let server; let server;
before(() => {
active = new ActiveConnector(mockConnection);
});
beforeEach((done) => { beforeEach((done) => {
active = new ActiveConnector(mockConnection);
sandbox = sinon.sandbox.create().usingPromise(Promise); sandbox = sinon.sandbox.create().usingPromise(Promise);
getNextPort() getNextPort()
@@ -31,9 +33,12 @@ describe('Connector - Active //', function () {
.listen(PORT, () => done()); .listen(PORT, () => done());
}); });
}); });
afterEach((done) => {
afterEach(() => {
sandbox.restore(); sandbox.restore();
server.close(done); server.close();
active.end();
}); });
it('sets up a connection', function () { it('sets up a connection', function () {
@@ -43,13 +48,27 @@ describe('Connector - Active //', function () {
}); });
}); });
it('destroys existing connection, then sets up a connection', function () { it('rejects alternative host', function () {
const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy'); 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) return active.setupConnection(host, PORT)
.then(() => { .then(() => {
expect(destroyFnSpy.callCount).to.equal(1); const destroyFnSpy = sandbox.spy(active.dataSocket, 'destroy');
expect(active.dataSocket).to.exist;
return active.setupConnection(host, PORT)
.then(() => {
expect(destroyFnSpy.callCount).to.equal(1);
expect(active.dataSocket).to.exist;
});
}); });
}); });