11

How can I prevent someone from simply doing

while(true){client.emit('i am spammer', true)};

This sure proves to be a problem when someone has the urge to crash my node server!

enter image description here

Justin
  • 400
  • 1
  • 5
  • 16
  • 1
    Would killing the connection after receiving a flood (certain number of messages in a short window of time) be acceptable? Of course, they could reconnect afterwards, but this shifts the problem into the realm of classic DoS protection. – zinga Mar 01 '14 at 07:45
  • http://stackoverflow.com/questions/16719749/how-can-i-prevent-malicious-use-of-my-sockets/20967971#20967971 – GiveMeAllYourCats Mar 01 '14 at 10:57
  • 1
    WebSockets, afterall, are just sockets. Typical DoS protection via firewall would likely suffice. You could also implement something like throttling sockets, and overtime if the socket remains at high traffic just gets dropped. Sockets have session affinity, so it actually makes monitoring and throttling sockets pretty easy. – tsturzl Mar 03 '14 at 22:21

3 Answers3

5

Like tsrurzl said you need to implement a rate limiter (throttling sockets).

Following code example only works reliably if your socket returns a Buffer (instead of a string). The code example assumes that you will first call addRatingEntry(), and then call evalRating() immediately afterwards. Otherwise you risk a memory leak in the case where evalRating() doesn't get called at all or too late.

var rating, limit, interval;

rating = []; // rating: [*{'timestamp', 'size'}]
limit = 1048576; // limit: maximum number of bytes/characters.
interval = 1000; // interval: interval in milliseconds.
// Describes a rate limit of 1mb/s

function addRatingEntry (size) {
    // Returns entry object.
    return rating[(rating.push({
        'timestamp': Date.now(),
        'size': size
    }) - 1);
}

function evalRating () {
// Removes outdated entries, computes combined size, and compares with limit variable.
// Returns true if you're connection is NOT flooding, returns false if you need to disconnect.
    var i, newRating, totalSize;
    // totalSize in bytes in case of underlying Buffer value, in number of characters for strings. Actual byte size in case of strings might be variable => not reliable.
    newRating = [];
    for (i = rating.length - 1; i >= 0; i -= 1) {
        if ((Date.now() - rating[i].timestamp) < interval) {
            newRating.push(rating[i]);
        }
    }
    rating = newRating;

    totalSize = 0;
    for (i = newRating.length - 1; i >= 0; i -= 1) {
        totalSize += newRating[i].timestamp;
    }

    return (totalSize > limit ? false : true);
}

// Assume connection variable already exists and has a readable stream interface
connection.on('data', function (chunk) {
    addRatingEntry(chunk.length);
    if (evalRating()) {
         // Continue processing chunk.
    } else {
         // Disconnect due to flooding.
    }
});

You can add extra checks, like checking whether or not the size parameter really is a number etc.

Addendum: Make sure the rating, limit and interval variables are enclosed (in a closure) per connection, and that they don't define a global rate (where each connection manipulates the same rating).

dot slash hack
  • 558
  • 1
  • 5
  • 13
3

I implemented a little flood function, not perfect (see improvements below) but it will disconnect a user when he does to much request.

// Not more then 100 request in 10 seconds
let FLOOD_TIME = 10000;
let FLOOD_MAX = 100;

let flood = {
    floods: {},
    lastFloodClear: new Date(),
    protect: (io, socket) => {

        // Reset flood protection
        if( Math.abs( new Date() - flood.lastFloodClear) > FLOOD_TIME ){
            flood.floods = {};
            flood.lastFloodClear = new Date();
        }

        flood.floods[socket.id] == undefined ? flood.floods[socket.id] = {} : flood.floods[socket.id];
        flood.floods[socket.id].count == undefined ? flood.floods[socket.id].count = 0 : flood.floods[socket.id].count;
        flood.floods[socket.id].count++;

        //Disconnect the socket if he went over FLOOD_MAX in FLOOD_TIME
        if( flood.floods[socket.id].count > FLOOD_MAX){
            console.log('FLOODPROTECTION ', socket.id)
            io.sockets.connected[socket.id].disconnect();
            return false;
        }

        return true;
    }
}

exports = module.exports = flood;

And then use it like this:

let flood = require('../modules/flood')

// ... init socket io...

socket.on('message', function () {
    if(flood.protect(io, socket)){
        //do stuff
    }   
});

Improvements would be, to add another value next to the count, how often he got disconneted and then create a banlist and dont let him connect anymore. Also when a user refreshes the page he gets a new socket.id so maybe use here a unique cookie value instead of the socket.id

kenny
  • 1,628
  • 20
  • 14
3

Here is simple rate-limiter-flexible package example.

const app = require('http').createServer();
const io = require('socket.io')(app);
const { RateLimiterMemory } = require('rate-limiter-flexible');

app.listen(3000);

const rateLimiter = new RateLimiterMemory(
  {
    points: 5, // 5 points
    duration: 1, // per second
  });

io.on('connection', (socket) => {
  socket.on('bcast', async (data) => {
    try {
      await rateLimiter.consume(socket.handshake.address); // consume 1 point per event from IP
      socket.emit('news', { 'data': data });
      socket.broadcast.emit('news', { 'data': data });
    } catch(rejRes) {
      // no available points to consume
      // emit error or warning message
      socket.emit('blocked', { 'retry-ms': rejRes.msBeforeNext });
    }
  });
});

Read more in official docs

Animir
  • 1,121
  • 10
  • 23