Firebase realtime DB - Create rule that limits size of child object - firebase-realtime-database

I've got a Realtime Database where users can write data to a certain key. However, I want to avoid a potentially malicious user from inserting a huge amount of data, without creating any new middleware.
Here are my rules right now:
{
"rules": {
/*
* Only let people read a specific name
* Only let people write "$user": <public info json> or "$user": null
*/
"$user": {
".read": "true",
".write": "auth.uid === $user"
".validate": "newData.val().length <= 123456 || newData.val() == null"
}
}
}
But this incorrectly rejects a write if the data at databaseurl/$user is a child object rather than a string, even if the JSON string representation of the child is below the length size.
How can I restrict the length of a child object, if the child is more than a simple string?
Thanks!

Related

realtime-database rule validation is not working when updating data with null

When I try to update data in realtime-database with null, ".validate" in database.rules.json seems to be ignored.
I have a database.rules.json in the following format (not the exact same way, but this shows what I expect at least).
database.rules.json
"data": {
".read": "auth != null",
".write": "auth != null",
".validate": "newData.val() != null"
}
When I update realtime-database with
frontend.js
import { set } from 'firebase/database';
export class PublishService {
...
setWithData(value) {
// suppose dbReference has ref to "data"
// something like ref(this.database, "data")
set(this.dbReference, value);
}
}
where value == null,
I can still update realtime-database with null even though this isn't what I expect.
Is this how realtime-database is supposed to work?
If that's the case, is there any documentation that says that?
The .validate rule is not triggered for data deletions. From the documentation on .validate rules:
the validate definitions are ignored when data is deleted (that is, when the new value being written is null).
So you'll want to check for newData.exists() in the .write rule.

Firebase Realtime Rules: Allowing multiple Users Access to Data

So I have my Database structured like this, the owner gets set when the group is created and the owner the should have the permission to add other Users to allowed so they can access and edit the data too.
-Groups
|-Groupname
|- Owner: string
|- Allowed: List<string>
|- Data: all the data
So my attempt were these rules but they dont work when I use the playground feature with a saved uid under owner or allowed:
"Groups" : {
"$group": {
".read": "auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').val() === auth.uid)",
".write": "auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').val() === auth.uid)"
}
}
And would a User still be able to create a new group when these rules would work?
Pictures of the Database and Errors:
First, in the Realtime Database, avoid using arrays and use a map instead.
Change this:
"Allowed": {
"0": "8ZiQGBPFkiZOLgLJBgDeLw9ie9D3",
"1": "KEuhrxnAWXS0dnotjhjFAYUOcm42",
"2": "48yULftKSxgyS84ZJC4hs4ug4Ei2"
}
to this:
"Allowed": {
"8ZiQGBPFkiZOLgLJBgDeLw9ie9D3": true,
"KEuhrxnAWXS0dnotjhjFAYUOcm42": true,
"48yULftKSxgyS84ZJC4hs4ug4Ei2": true
}
Read that linked blog post for more info, but in short, it makes adding/removing users really simple:
const groupRef = firebase.database.ref(`Groups/${groupId}`);
// add a user
groupRef.child("E04HLbIjGDRUQxsRReHSKifaXIr2").set(true);
// remove a user
groupRef.child("KEuhrxnAWXS0dnotjhjFAYUOcm42").remove();
You can also change true to whatever you want. Here are some examples:
false = participant, true = moderator
false = read-only, true = can edit
Role names: "member", "admin", "moderator", etc.
Privilege levels: 0 (member), 500 (moderator), 1000 (owner), etc. (make sure to space these out, you don't want to have to add in a level between 0 and 1 and have to edit your entire database).
The most important point though, is that Realtime Database security rules don't know about arrays. data.val() won't return an array, it will just return a sentinel value that says "non-null object is here!". This means a map is necessary for security rules.
This reference document covers the structure and variables you can use in your Realtime Database Security Rules.
With your proposed rules, you attempt to allow any user in the group to be able to write to the group's data - but you don't manage what they can and can't write to. Any malicious member of a group could add/delete anyone else, make themselves the owner, or even delete the group entirely.
{
"rules": {
"Groups" : {
"$group": {
// If this group doesn't exist, allow the read.
// If the group does exist, only the owner & it's members
// can read this group's entire data tree.
".read": "!data.exists() || (auth != null && (data.child('Owner').val() === auth.uid || data.child('Allowed').child(auth.uid).val() === true))",
"Owner": {
// Only the current owner can write data to this key if it exists.
// If the owner is not yet set, they can only claim it for themselves.
".write": "auth != null && (data.val() === auth.uid || (!data.exists() && newData.val() === auth.uid))",
// Force this value to be a string
".validate": "newData.isString()"
},
"Allowed": {
// Only the owner can edit the entire member list
// For a new group, the owner is also granted write access
// for it's creation
".write": "auth != null && (data.parent().child('Owner').val() === auth.uid || (!data.exists() && newData.parent().child('Owner').val() === auth.uid))",
"$member": {
// Allows the user to remove themselves from the group
".write": "auth != null && auth.uid === $member && !newData.exists()",
// Force this value to be a boolean
".validate": "newData.isBoolean()"
}
},
"Data": {
// The owner and members can edit anything under "Data"
// Currently this includes deleting everything under it!
// For a new group, the owner is also granted write access
// for it's creation
// TODO: tighten structure of "Data" like above
".write": "auth != null && (data.parent().child('Owner').val() === auth.uid || data.parent().child('Allowed').child(auth.uid).val() === true || (!data.exists() && newData.parent().child('Owner').val() === auth.uid))"
}
}
}
}
}

How can you observe a snapshot of a upper level child w/o overriding the lower child rules?

JSON
"users" : {
"02PdiNpmW3MMyJt3qPuRyTpHLaw2" : {
"Coordinates" : {
"latitude" : -24.809620667034363,
"longitude" : 28.321706241781342
},
"Education" : "6", ........./// here are 10 Childs further on same level as education
"Music"
The current rules
{
"rules": {
".read": false,
".write": false,
, "users": {
"$uid": {
"Education" :{
".read": "$uid == auth.uid",
".write": "$uid == auth.uid"
}
According to my understanding, if I make users read rule for authorized users, it will override the education rule. This below is where users read rule is needed
let artist = Database.database().reference().child("users").queryOrdered(byChild: "music").queryStarting(atValue:dateToday.timeIntervalSince1970*1000)
artist.observe(DataEventType.value, with: { snapshot in
Update: I had a typo "users" at the rules
In general indeed the rule is that if you grant somebody read (or write) access to a node, they have the same access to all data under that node. So you can't just say ".read": true on /people as that will them to read the entire node.
What you can do is specify what query is allowed in your security rules. So to allow every to read all nodes with the given music value, you could do:
"baskets": {
".read": "query.orderByChild == 'music' &&
query.equalTo > 1354867200000"
}
A few things to keep in mind here:
I hard-coded it here, but you will have to calculate the value for the filter in your rules somehow based on the now variable.
You can only filter on a single value, so you can't filter on both music and uid. You may be able to merge the two values into a single property and order/filter on that. For more on this see: Query based on multiple where clauses in Firebase

Controlling data-update on Firebase

I am using Firebase for a small iOS project and I wonder if the following is possible.
Under a given node "myNode_123" I store data records; each record having the shape below:
[fieldStr: "ABC_xyz", fieldNum: 678]
A record is in fact a subnode.
Now my question is about updating the data, for already existing records.
Someone should be allowed to update a data record only if the new value for fieldNum is higher than the one already there.
And in any case the value for fieldStr should stay as it is.
I have already written some rules to make sure fieldNum is fullfilling my request.
But I still have the issue of fieldStr having the risk of being changed.
Here are my rules:
{
"rules": {
".read": true,
//".write": true
"myNode_123": {
"$Record": {
// Ensure that we are either creating a new record not yet existing,
// or that we are only updating for a higher fieldNum.
".write": "(!root.child('myNode_123/'+$Record).exists()) ||
(root.child('myNode_123/'+$Record).exists() && (newData.child('fieldNum').val() > data.child('fieldNum').val()))"
}
}
}
}
The control of fieldNum is working as I wish. But it is still possible to change fieldStr, which I do not want.
Any advice from a Firebase expert?
Add this under $Record.
{
"rules": {
".read": true,
//".write": true
"myNode_123": {
"$Record": {
// Ensure that we are either creating a new record not yet existing,
// or that we are only updating for a higher fieldNum.
".write": "!data.exists() || newData.child('fieldNum').val() > data.child('fieldNum').val()",
"fieldStr": {
".validate": "!data.exists() || data.val() == newData.val()"
}
}
}
}
}
The !data.exists() will make sure only new data can be written to this location. And data.val() == newData.val() will add an exception to allow writes when the new data is the same as the old data, just in case you want to write the entire object to the Database and include the fieldStr.

FireBase - maintain/guarantee data consistency

I'm trying to understand what is the right approach for this following scenario :
Multiplayer game,each game structured only with two players. Each game/match will be completely randomized
Lets assume 5 users "logs" the same time into my app, each one of them "searching" for a match. Each user hold a property named opponent which equal the the opponent uniqueID(initial value equal "". so far so good.
assuming user 1 matched with user 3. user 1 will update his own oppoent value to user 3 uniqueID and will do the same to user 3
Problem
1) What if at the same moment, user 2 tried to to the same to user 3?
2) What if at the same moment, user 3 tried to do so to user 4?
Main Point
Is it possible to "lock" a user values? or freeze them once they changed? Am i going in the wrong approach?
I was thinking using Security Rules and Validation in order to create consistency but i just may picked the wrong tech(FireBase). Any thoughts?
EDIT
Security rules i have tried, which still for some reason enable a third device change "already changed opponent" value.
{
"rules": {
".read": true,
".write": true,
"Users" :
{
"$uid" : {
"opponent" :
{
".write" : "data.val() == 'empty' || data.val() == null",
".validate": "data.val() == null || data.val() == 'empty' || newData.parent().parent().child(newData.val())
.child('opponent').val() == $uid"
}
,".indexOn": "state"
}
}
}
}
You can validate many things with Firebase security rules.
For example, you can say that an opponent can only be written if there currently is no opponent for the user:
"users": {
"$uid": {
"opponent: {
".write": "!data.exists()"
}
}
}
With this and the following operations:
ref.child('users').child(auth.uid).child('opponent').set('uid:1234');
ref.child('users').child(auth.uid).child('opponent').set('uid:2345');
The second set() operation will fail, because the opponent property already has a value at that point.
You can expand that to also validate that the opponents must refer to each other:
"users": {
"$uid": {
"opponent: {
".write": "!data.exists()"
".validate": "newData.parent().parent().child(newData.val())
.child('opponent').val() == $uid"
}
}
}
From the opponent that is being written, we go up two levels back to users: newData.parent().parent().
Then we go down into the opponent's node: child(newData.val()).
And we then validate that the opponent's opponent property matches our uid: child('opponent').val() == $uid.
Now both of the write operations from above will fail, because they're only setting the opponent one at a time. To fix this, you'll need to perform a so-called multi-location update:
var updates = {};
updates['users/'+auth.uid+'/opponent'] = 'uid:1234';
updates['users/uid:1234/opponent'] = auth.uid;
ref.update(updates);
We're now sending a single update() command to the Firebase server that writes the uids to both opponents. This will satisfy the security rule.
A few notes:
these are just some examples to get you started. While they should work, you'll need to write your own rules that meet your security needs.
these rules just handle writing of opponents. You'll probably also want to testing what happens when the game is over and you need to clear the opponents.
You might also look at the transaction operation.
Firebase transactions make sure that the current set of data you are acting on is really what is in the database, guaranteeing that you are updating data that is in the right condition. The docs indicate that this is the recommended way to avoid race conditions such as you describe.
Something like this (in IOS, and warning - not tested):
NSString* user1Key = #"-JRHTHaIs-jNPLXOQivY";
NSString* user2Key = #"-NFHUaIs-kNPLJDHuvY";
Firebase *user1Ref = [[Firebase alloc] initWithUrl: #"https://docs-examples.firebaseio.com.users/-JRHTHaIs-jNPLXOQivY/opponent"];
Firebase *user2Ref = [[Firebase alloc] initWithUrl: #"https://docs-examples.firebaseio.com.users/-NFHUaIs-kNPLJDHuvY/opponent"];
//See if the proposed opponent does not yet have a match
[user2Ref runTransactionBlock:^FTransactionResult *(FMutableData *opponent) {
if (opponent.value == [NSNull null]) {
//They have no match - update with our key and signal success
[opponent setValue:user1Key];
return [FTransactionResult successWithValue: opponent];
} else {
return [FTransactionResult abort]; //They already have an opponent - fail
//Notify the user that the match didn't happen
}
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
if (!error && committed) {
//The transaction above was committed with no error
//Update our record with the other player - we're matched!
[user1ref setValue:user2Key];
//Do whatever notification you want
} else {
//Notify that the matchup failed
}
}];

Resources