Is the Way the Firebase Database Quickstart Handles Counts Secure

Is the way the Firebase database quickstart handles counts secure?

The security rules can do a few things:

  • ensure that a user can only add/remove their own uid to the stars node

    "stars": {
    "$uid": {
    ".write": "$uid == auth.uid"
    }
    }
  • ensure that a user can only change the starCount when they are adding their own uid to the stars node or removing it from there

  • ensure that the user can only increase/decrease starCount by 1

Even with these, it might indeed still be tricky to have a security rule that ensures that the starCount is equal to the number of uids in the stars node. I encourage you to try it though, and share your result.

The way I've seen most developers deal with this though is:

  • do the start counting on the client (if the size of the stars node is not too large, this is reasonable).
  • have a trusted process running on a server that aggregates the stars into starCount. It could use child_added/child_removed events for incrementing/decrementing.

Update: with working example

I wrote up a working example of a voting system. The data structure is:

votes: {
uid1: true,
uid2: true,
},
voteCount: 2

When a user votes, the app sends a multi-location update:

{
"/votes/uid3": true,
"voteCount": 3
}

And then to remove their vote:

{
"/votes/uid3": null,
"voteCount": 2
}

This means the app needs to explicitly read the current value for voteCount, with:

function vote(auth) {
ref.child('voteCount').once('value', function(voteCount) {
var updates = {};
updates['votes/'+auth.uid] = true;
updates.voteCount = voteCount.val() + 1;
ref.update(updates);
});
}

It's essentially a multi-location transaction, but then built in app code and security rules instead of the Firebase SDK and server itself.

The security rules do a few things:

  1. ensure that the voteCount can only go up or down by 1
  2. ensure that a user can only add/remove their own vote
  3. ensure that a count increase is accompanied by a vote
  4. ensure that a count decrease is accompanied by a "unvote"
  5. ensure that a vote is accompanied by a count increase

Note that the rules don't:

  • ensure that an "unvote" is accompanied by a count decrease (can be done with a .write rule)
  • retry failed votes/unvotes (to handle concurrent voting/unvoting)

The rules:

"votes": {
"$uid": {
".write": "auth.uid == $uid",
".validate": "(!data.exists() && newData.val() == true &&
newData.parent().parent().child('voteCount').val() == data.parent().parent().child('voteCount').val() + 1
)"
}
},
"voteCount": {
".validate": "(newData.val() == data.val() + 1 &&
newData.parent().child('votes').child(auth.uid).val() == true &&
!data.parent().child('votes').child(auth.uid).exists()
) ||
(newData.val() == data.val() - 1 &&
!newData.parent().child('votes').child(auth.uid).exists() &&
data.parent().child('votes').child(auth.uid).val() == true
)",
".write": "auth != null"
}

jsbin with some code to test this: http://jsbin.com/yaxexe/edit?js,console

firebase database equivalent of MySQL transaction

Firebase Database transactions perform an update to a single location based on the current value of that same location. They explicitly do not work across multiple locations, since that would limit their scalability. Sometimes developers work around this by performing a transaction higher up in their JSON tree (at the first common point of the locations). I'd recommend against that, as that would limit the scalability even further.

The only way to efficiently update multiple locations with one API call, is with a multiple location update. This does however not have reading of the current value built-in.

So if you want to update multiple locations based on their current value, you'll have to perform the read operation in your application code, turn that into a multi-location update, and then use security rules to ensure all of those updates follow your application rules. This is a quite non-trivial approach, so I hardly see it being done in practice. See my answer here for an example: Is the way the Firebase database quickstart handles counts secure?

How can I use a transaction while performing a multi-location update in Firebase?

No. Transactions function on a single node.

That means that if you want to run a multi-location transaction, you will essentially have to run that transaction at the "lowest level shared node" in the tree. That is hardly ever a good idea.

The alternative would be to secure your multi-location write using server-side security rules. For an example of this, see Is the way the Firebase database quickstart handles counts secure?, which essentially builds a compare-and-set operation using security rules.

Firebase/NoSQL Database: Update stats realtime or compute them?

In Firebase (and most NoSQL databases) you often store the data for the way that your application consumes it.

So if you are trying to track the number of check-ins at a place, then you should at the very least track the check-ins for each place.

PlaceCheckins
CoffeePlace
Fred: true
Tom: true
Lisa: true

If you also want to show the places where a specific user has checked in, you'll keep the same data per user (as you already had).

UserCheckins
Fred:
CoffeePlace: true
Tom:
CoffeePlace: true
Lisa:
CoffeePlace: true

This type of data duplication is very normal in Firebase (and again: NoSQL databases in general). Disk space is cheap, the user's time is not. By duplicating the data, you ensure that you can get exactly what you need for a specific screen in your app.

To keep both lists in sync, you'd use multi-location updates or transactions (depending a bit on how you structure the data). A checkin by Jenny into the coffee place could be coded in JavaScript as:

var updates = {};
updates['/PlaceCheckins/CoffeePlace/Jenny'] = true;
updates['/UserCheckins/Jenny/CoffeePlace'] = true;
ref.update(updates);

If you also want to display the count of check-ins (for a specific user of a specific place), you either download the check-ins and tally client-side or you keep a total in the database. If you take the latter approach, have a look at my answer on Is the way the Firebase database quickstart handles counts secure?

Finally: read this article for a great (non-Firebase specific) introduction to NoSQL data modeling.



Related Topics



Leave a reply



Submit