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).
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.
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.
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.
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); } }); }) }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With