Skip to content

Commit

Permalink
Merge pull request kirm#124 from garronej/master
Browse files Browse the repository at this point in the history
Fixing vunerability to flood with statefull connections.
  • Loading branch information
kirm authored Sep 10, 2017
2 parents 41ca893 + f626f17 commit fdf16c9
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 9 deletions.
2 changes: 2 additions & 0 deletions doc/api.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Starts SIP protocol.
* `publicAddress`, `hostname` - address and hostname to be used within sip.js generated local uris and via headers. Sip.js will use `options.publicAddress` when
it's defined, then fallback to `options.hostname` and the fallback to value returned by node.js `os.hostname()` API.
* `ws_port` - port for WebSockets transport. To enable WebSockets transport, this field is required; no default provided.
* `maxBytesHeaders` - (For TCP and TLS ) Max allowed length in bytes of a SIP message headers ( without content ). ; default: 60480.
* `maxContentLength` - (For TCP and TLS ) Max allowed content length for a SIP message. ; default: 604800.


`onRequest` - callback to be called on new request arrival. It is expected to be a function of two arguments
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"homepage": "http://github.com/kirm/sip.js",
"author": "Kirill Mikhailov <[email protected]>",
"main": "sip",
"scripts": {
"test": "./node_modules/.bin/coffee test/runtests.coffee"
},
"directories": {
"lib": "",
"example": "./examples",
Expand Down
54 changes: 48 additions & 6 deletions sip.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function parseParams(data, hdr) {
var re = /\s*;\s*([\w\-.!%*_+`'~]+)(?:\s*=\s*([\w\-.!%*_+`'~]+|"[^"\\]*(\\.[^"\\]*)*"))?/g;

for(var r = applyRegex(re, data); r; r = applyRegex(re, data)) {
hdr.params[r[1].toLowerCase()] = r[2];
hdr.params[r[1].toLowerCase()] = r[2] || null;
}

return hdr;
Expand Down Expand Up @@ -435,19 +435,44 @@ function defaultPort(proto) {
return proto.toUpperCase() === 'TLS' ? 5061 : 5060;
}

function makeStreamParser(onMessage) {
function makeStreamParser(onMessage, onFlood, maxBytesHeaders, maxContentLength) {

onFlood= onFlood || function(){};
maxBytesHeaders= maxBytesHeaders || 60480;
maxContentLength= maxContentLength || 604800;

var m;
var r = '';

function headers(data) {
r += data;

if( r.length > maxBytesHeaders ){

r = '';

onFlood();

return;

}

var a = r.match(/^\s*([\S\s]*?)\r\n\r\n([\S\s]*)$/);

if(a) {
r = a[2];
m = parse(a[1]);

if(m && m.headers['content-length'] !== undefined) {

if (m.headers['content-length'] > maxContentLength) {

r = '';

onFlood();

}

state = content;
content('');
}
Expand All @@ -474,6 +499,7 @@ function makeStreamParser(onMessage) {
var state=headers;

return function(data) { state(data); }

}
exports.makeStreamParser = makeStreamParser;

Expand Down Expand Up @@ -508,7 +534,7 @@ function checkMessage(msg) {
msg.headers.cseq;
}

function makeStreamTransport(protocol, connect, createServer, callback) {
function makeStreamTransport(protocol, maxBytesHeaders, maxContentLength, connect, createServer, callback) {
var remotes = Object.create(null);
var flows = Object.create(null);

Expand All @@ -522,15 +548,27 @@ function makeStreamTransport(protocol, connect, createServer, callback) {
flows[flowid] = remotes[remoteid];
}

stream.setEncoding('binary');
stream.on('data', makeStreamParser(function(m) {
var onMessage= function(m) {

if(checkMessage(m)) {
if(m.method) m.headers.via[0].params.received = remote.address;
callback(m,
{protocol: remote.protocol, address: stream.remoteAddress, port: stream.remotePort, local: { address: stream.localAddress, port: stream.localPort}},
stream);
}
}));

};

var onFlood= function() {

console.log("Flood attempt, destroying stream");

stream.destroy();

};

stream.setEncoding('binary');
stream.on('data', makeStreamParser( onMessage, onFlood, maxBytesHeaders, maxContentLength));

stream.on('close', function() {
if(flowid) delete flows[flowid];
Expand Down Expand Up @@ -594,6 +632,8 @@ function makeStreamTransport(protocol, connect, createServer, callback) {
function makeTlsTransport(options, callback) {
return makeStreamTransport(
'TLS',
options.maxBytesHeaders,
options.maxContentLength,
function(port, host, callback) { return tls.connect(port, host, options.tls, callback); },
function(callback) {
var server = tls.createServer(options.tls, callback);
Expand All @@ -606,6 +646,8 @@ function makeTlsTransport(options, callback) {
function makeTcpTransport(options, callback) {
return makeStreamTransport(
'TCP',
options.maxBytesHeaders,
options.maxContentLength,
function(port, host, callback) { return net.connect(port, host, callback); },
function(callback) {
var server = net.createServer(callback);
Expand Down
51 changes: 51 additions & 0 deletions test/messages/_compute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use strict";
/*
Compute .json
This script will parse the raw sip messages *.dat that are present
in this directory and export the result in a file *.json.
Note that an empty file .json has to be present in the dir.
Be careful, check that the .json generated match the value
that is actually expected.
*/

let fs= require("fs");
let path = require("path");
let sip = require("../../");

let encoding= "binary";

let files = fs.readdirSync(__dirname);

let names = [];

for (let file of files) {

if (path.extname(file) !== ".json") continue;

names.push(path.basename(file, ".json"));

}

for (let name of names) {

console.log(`${name}.dat => sip.parse => ${name}.json`);

let dat = fs.readFileSync(
path.join(__dirname, `${name}.dat`),
encoding
);

let json = JSON.stringify(sip.parse(dat), null, 2);

fs.writeFileSync(
path.join(__dirname, `${name}.json`),
json,
{ "encoding": encoding }
);

}

console.log("DONE");
3 changes: 2 additions & 1 deletion test/messages/longreq.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"uri": "sip:amazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallername@example.net",
"params": {
"tag": "12982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982424",
"unknownheaderparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename": "unknowheaderparamvaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevalue"
"unknownheaderparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename": "unknowheaderparamvaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevalue",
"unknownvaluelessparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamname": null
}
},
"call-id": "longreq.onereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongcallid",
Expand Down
3 changes: 2 additions & 1 deletion test/messages/mpart01.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"host": "127.0.0.1",
"port": 5070,
"params": {
"branch": "z9hG4bK-d87543-4dade06d0bdb11ee-1--d87543-"
"branch": "z9hG4bK-d87543-4dade06d0bdb11ee-1--d87543-",
"rport": null
}
}
],
Expand Down
1 change: 1 addition & 0 deletions test/messages/wsinv.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"uri": "sip:[email protected]",
"params": {
"newparam": "newvalue",
"secondparam": null,
"q": "0.33"
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/runtests.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ runTests = (tests) ->
modules = process.argv[2..process.argv.length]

if modules.length == 0
modules = ['parser', 'digest', 'rport']
modules = ['parser', 'digest', 'rport', 'stream_parser']

console.log modules

Expand Down
122 changes: 122 additions & 0 deletions test/stream_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use strict";

let sip = require("../");
let fs = require("fs");
let path= require("path");

function handleKeepAlive(success) {

let isSuccess= false;

let rawMessageAsBinaryString= fs.readFileSync(path.join(__dirname, "messages", "baddate.dat"), "binary");

let streamParser = sip.makeStreamParser(
function (sipPacket) {

isSuccess= true;

console.log("ok test handleKeepAlive");

success();

}
);

for( let i=0; i<20000; i++)
streamParser("\r\n");

streamParser(rawMessageAsBinaryString);

console.assert(isSuccess, "Message has not been parsed");

}

function flood(success) {

let isSuccess= false;

let onMessage= function(sipPacket){

console.assert(false, "Message should not have been parsed");

};

let onFlood= function(){

isSuccess= true;

console.log("ok test flood");

success();

}

let maxBytesHeaders= 6048;

let streamParser = sip.makeStreamParser(onMessage, onFlood, maxBytesHeaders);

let floodData= "";

for (let i = 0; i < maxBytesHeaders; i++){

floodData+= "x";

}

streamParser(floodData);

streamParser("OVERFLOW!");

console.assert(isSuccess, "We have been buffering flood data");

}

function payloadFlood(success) {

let isSuccess = false;

let split = fs
.readFileSync(
path.join(__dirname, "messages", "baddate.dat"),
"binary"
)
.split("\r\n");

for (let i = 0; i <= split.length; i++)
if (split[i].match(/^Content-Length:\ ([0-9]+)$/)) {
split[i] = split[i].replace(/[0-9]+/, "999999999999");
break;
}


let rawMessageAsBinaryString = split.join("\r\n");

let onMessage= function(sipPacket){

console.assert(false, "Message should not have been parsed");

};

let onFlood= function(){

isSuccess= true;

console.log("ok test payloadFlood");

success();

}

let streamParser = sip.makeStreamParser(onMessage, onFlood);

streamParser(rawMessageAsBinaryString);

for( let i=0; i<100000; i++)
streamParser("FLOOD");

console.assert(isSuccess, "Payload Flood attack!");

}


exports.tests= [ handleKeepAlive, flood, payloadFlood ];

0 comments on commit fdf16c9

Please sign in to comment.