Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the way the Firebase database quickstart handles counts secure?

I want to create an increment field for article likes.

I am referring to this link: https://firebase.google.com/docs/database/android/save-data#save_data_as_transactions

In the example there is code for increment field:

if (p.stars.containsKey(getUid())) {
    // Unstar the post and remove self from stars
    p.starCount = p.starCount - 1;
    p.stars.remove(getUid());
} else {
    // Star the post and add self to stars
    p.starCount = p.starCount + 1;
    p.stars.put(getUid(), true);
}

But how can I be sure if the user already liked/unliked the article?

In the example, user (hacker) might as well clear whole stars Map like this and it will save anyway:

p.stars = new HashMap<>();

and it will ruin the logic for other users who were already liked it.

I do not even think you can make rules for this, especially for "decrease count" action.

Any help, suggestions?

like image 849
Zigmārs Dzērve Avatar asked Jun 21 '16 20:06

Zigmārs Dzērve


People also ask

How secure is Firebase database?

As a default Firebase database has no security, it's the development team's responsibility to correctly secure the database prior to it storing real data. In Google Firebase, this is done by requiring authentication and implementing rule-based authorization for each database table.

How do you secure a Firebase rule?

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.

Can anyone access my Firebase database?

These rules live on the Firebase servers and are enforced automatically at all times. Every read and write request will only be completed if your rules allow it. By default, your rules do not allow anyone access to your database.


1 Answers

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

like image 58
Frank van Puffelen Avatar answered Oct 15 '22 05:10

Frank van Puffelen