Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 830
Brian Mayer Avatar asked Jul 18 '14 16:07

Brian Mayer


People also ask

What are security rules in Firebase?

Firebase Security Rules work by matching a pattern against database paths, and then applying custom conditions to allow access to data at those paths. All Rules across Firebase products have a path-matching component and a conditional statement allowing read or write access.

Is Firebase good for security?

The short answer is yes: by authenticating your users and writing security rules, you can fully restrict read / write access to your Firebase data. In a nutshell, Firebase security is enforced by server-side rules, that you author, and govern read or write access to given paths in your Firebase data tree.

Does Firebase have a limit?

While not a hard limit, if you sustain more than 1,000 writes per second, your write activity may be rate-limited. 256 MB from the REST API; 16 MB from the SDKs. The total data in each write operation should be less than 256 MB. Multi-path updates are subject to the same size limitation.


2 Answers

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);             }         });     }) } 
like image 81
Kato Avatar answered Oct 01 '22 05:10

Kato


I don't have enough reputations to write in the comment, but I agree to Victor's comment. If you insert the fb.child('messages').push(...) into a loop (i.e. for (let i = 0; i < 100; i++) {...} ) then it would successfully push 60-80 meessages ( in that 500ms window frame.

Inspired by Kato's solution, I propose a modification to the rules as follow:

rules: {   users: {     "$uid": {       "timestamp": { // similar to Kato's answer         ".write": "auth.uid === $uid && newData.exists()"         ,".read": "auth.uid === $uid"         ,".validate": "newData.hasChildren(['time', 'key'])"         ,"time": {           ".validate": "newData.isNumber() && newData.val() === now && (!data.exists() || newData.val() > data.val() + 1000)"         }         ,"key": {          }       }       ,"messages": {         "$key": { /// this key has to be the same is the key in timestamp (checked by .validate)            ".write": "auth.uid === $uid && !data.exists()" ///only 'create' allow            ,".validate": "newData.hasChildren(['message']) && $key === root.child('/users/' + $uid + '/timestamp/key').val()"            ,"message": { ".validate": "newData.isString()" }            /// ...and any other datas such as 'time', 'to'....         }       }     }   } } 

The .js code is quite similar to Kato's solution, except that the getTimestamp would return {time: number, key: string} to the next callback. Then we would just have to ref.update({[key]: data})

This solution avoids the 500ms time-window, we don't have to worry that the client must be fast enough to push the message within 500ms. If multiple write requests are sent (spamming), they can only write into 1 single key in the messages. Optionally, the create-only rule in messages prevents that from happening.

like image 43
ChiNhan Avatar answered Oct 01 '22 04:10

ChiNhan