Node socket.io, anything to prevent flooding?

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).


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


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