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 296573a commit e449e75
Show file tree
Hide file tree
Showing 10 changed files with 52 additions and 19 deletions.
8 changes: 6 additions & 2 deletions src/commands/registration/eprt.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion src/commands/registration/pasv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}}',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/registration/port.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>',
Expand Down
6 changes: 6 additions & 0 deletions src/connector/active.js
Original file line number Diff line number Diff line change
@@ -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.on('error', (err) => this.server && this.server.emit('client-error', {connection: this.connection, context: 'dataSocket', error: err}));
this.dataSocket.connect({host, port, family}, () => {
Expand Down
2 changes: 1 addition & 1 deletion src/connector/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 comment has been minimized.

Copy link
@Jl14Salvador

Jl14Salvador Aug 17, 2020

do we care about this return value? This returns a net.socket object. But this change will now just return back a boolean.
Something like this retains the current behaviour.

...
this.dataSocket.end(() => socket ? socket.destroy() : null );  //return null  and do nothing or throw error? 

This comment has been minimized.

Copy link
@trs

trs Aug 17, 2020

Contributor

Nope, return type doesn't matter for this callback.

This comment has been minimized.

Copy link
@matt-forster

matt-forster Aug 17, 2020

Contributor

The return value gets dropped, because its a callback that gets hooked into the finish event;

If provided, the optional callback function is attached as a listener for the 'finish' and the 'error' event.

https://nodejs.org/api/stream.html#stream_writable_end_chunk_encoding_callback

This comment has been minimized.

Copy link
@Jl14Salvador

Jl14Salvador Aug 17, 2020

👍

this.dataSocket = null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/commands/registration/eprt.spec.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
39 changes: 29 additions & 10 deletions test/connector/active.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 () {
Expand All @@ -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;
});
});
});

Expand Down

0 comments on commit e449e75

Please sign in to comment.