In my database, I have a users node that contains data under a user ID. This includes their bio, number of followers, whether the user is a moderator, and more.
users
{
userId1
{
bio: "Example bio..."
followers: 250
moderator: true
...
}
}
In order for the number of followers to be correct, I use a transaction block to increment the followers property every time the follow button is clicked. There are a few other properties that require transaction blocks as well.
Unfortunately, I have discovered that in order for the transactions to work, the security rules for the $userId node must be set to: “.write”: “auth != null”
. Otherwise, the number of followers property won’t be incremented when someone clicks the follow button. Because the transaction block queries the entire user, we can’t limit the security rules to just the “followers” property.
"users":
{
"$userId":
{
// Has to be set like this or transactions won't work
".read": "auth != null",
".write": "auth != null",
"bio":
{
// This will have no effect due to rule cascading
".write": "auth.uid === $userId"
}
"moderator":
{
// This will have no effect due to rule cascading
".write": ...
}
}
}
And since rules cascade, this makes it seem impossible to set specific rules for any other properties under user, including bio and whether the user is a moderator etc. This makes the user property vulerable to changes by malicious users.
The same thing happens for a post and likes, the example used in the Firebase documentation. Because the transaction block queries the entire post, we can’t limit the security rules to just the “likes” property. All of the other post properties will have to settle for the “.write”: “auth !=null”
setting because of cascading.
The best I can do is to use validation, but that won’t stop malicious users from setting their follow count to 10,000 or making themselves a moderator if they somehow gain access.
Using Firebase Rules, is there any way to secure nodes that have transactions run on them?
Edit: More Info
This is a simplified version of what my transaction block looks like for incrementing the follower count:
// Run transaction block on the user in the "users" node
userRef.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
// Store the user
if var user = currentData.value as? [String: AnyObject]
{
// Get the number of followers
var numberOfFollowers = user["numberOfFollowers"] as? Int ?? 0
// Increase the number of followers by 1
numberOfFollowers += 1
// Set the new number of followers
user["numberOfFollowers"] = numberOfFollowers as AnyObject?
// Set the user value and report transaction success
currentData.value = user
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
})
This is how followers are stored in my database:
myDatabase: {
followers: {
"andrew098239101": {
// These are all the user ID's of users that follow "andrew098239101"
"robert12988311": true
"sarah9234298347": true
"alex29101922": true
"greg923749232": true
}
"robert12988311": {
"alex29101922": true
}
}
...
users: {
"andrew098239101": {
// Andrew's user info
"bio": "hello I am Andrew"
"numberOfFollowers": 4
"moderator": true
...
}
"robert12988311": {
"bio": "I'm Robert"
"numberOfFollowers": 1
"moderator": false
...
}
}
}
There is a similar node for following, etc.