Search code examples
firebasefirebase-realtime-databasefirebase-securityrate-limiting

Firebase rate limiting in security rules?


I launched my first open repository project, EphChat, and people promptly started flooding it with requests.

Does Firebase have a way to rate limit requests in the security rules? I assume there's a way to do it using the time of the request and the time of previously written data, but can't find anything in the documentation about how I would do this.

The current security rules are as follows.

{
    "rules": {
      "rooms": {
        "$RoomId": {
          "connections": {
              ".read": true,
              ".write": "auth.username == newData.child('FBUserId').val()"
          },
          "messages": {
            "$any": {
            ".write": "!newData.exists() || root.child('rooms').child(newData.child('RoomId').val()).child('connections').hasChild(newData.child('FBUserId').val())",
            ".validate": "newData.hasChildren(['RoomId','FBUserId','userName','userId','message']) && newData.child('message').val().length >= 1",
            ".read": "root.child('rooms').child(data.child('RoomId').val()).child('connections').hasChild(data.child('FBUserId').val())"
            }
          },
          "poll": {
            ".write": "auth.username == newData.child('FBUserId').val()",
            ".read": true
          }
        }
      }
    }
}

I would want to rate-limit writes (and reads?) to the db for the entire Rooms object, so only 1 request can be made per second (for example).


Solution

  • The trick is to keep an audit of the last time a user posted a message. Then you can enforce the time each message is posted based on the audit value:

    {
      "rules": {
              // this stores the last message I sent so I can throttle them by timestamp
          "last_message": {
            "$user": {
              // timestamp can't be deleted or I could just recreate it to bypass our throttle
              ".write": "newData.exists() && auth.uid === $user",
              // the new value must be at least 5000 milliseconds after the last (no more than one message every five seconds)
              // the new value must be before now (it will be since `now` is when it reaches the server unless I try to cheat)
              ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val()+5000)"
            }
          },
    
          "messages": {
            "$message_id": {
              // message must have a timestamp attribute and a sender attribute
              ".write": "newData.hasChildren(['timestamp', 'sender', 'message'])",
              "sender": {
                ".validate": "newData.val() === auth.uid"
              },
              "timestamp": {
                // in order to write a message, I must first make an entry in timestamp_index
                // additionally, that message must be within 500ms of now, which means I can't
                // just re-use the same one over and over, thus, we've effectively required messages
                // to be 5 seconds apart
                ".validate": "newData.val() >= now - 500 && newData.val() === data.parent().parent().parent().child('last_message/'+auth.uid).val()"
              },
              "message": {
                ".validate": "newData.isString() && newData.val().length < 500" 
              },
              "$other": {
                ".validate": false 
              }
            }
          } 
      }
    }
    

    See it in action in this fiddle. Here's the gist of what's in the fiddle:

    var fb = new Firebase(URL);
    var userId; // log in and store user.uid here
    
    // run our create routine
    createRecord(data, function (recordId, timestamp) {
       console.log('created record ' + recordId + ' at time ' + new Date(timestamp));
    });
    
    // updates the last_message/ path and returns the current timestamp
    function getTimestamp(next) {
        var ref = fb.child('last_message/' + userId);
        ref.set(Firebase.ServerValue.TIMESTAMP, function (err) {
            if (err) { console.error(err); }
            else {
                ref.once('value', function (snap) {
                    next(snap.val());
                });
            }
        });
    }
    
    function createRecord(data, next) {
        getTimestamp(function (timestamp) {
            // add the new timestamp to the record data
            var data = {
              sender: userId,
              timestamp: timestamp,
              message: 'hello world'
            };
    
            var ref = fb.child('messages').push(data, function (err) {
                if (err) { console.error(err); }
                else {
                   next(ref.name(), timestamp);
                }
            });
        })
    }