3
votes

I am trying to read a path in my Firebase Realtime database /users/uid1 where the value at the path is null. In effect, it does not exist. My security rules allow reading at the path (rules below). When actually reading data, permission is denied.

Security rules are as follows:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "root.child('users').child($uid).child('mUser_Role').child('mRole_PermissionLevel').val() > 400 || root.child('users').child(auth.token.phone_number).child('mUser_Role').child('mRole_PermissionLevel').val() == 500"
      }
    }
  }
}

Trying to read /users/uid1 with /users/myuid/mUser_Role/mRole_PermissionLevel == 500 being true fails with permission denied.

How does Firebase determine that permission should be denied for the case when value is null ? Shouldn't this read be allowed and return a dataSnapshot where dataSnapshot.exists() returns false ?

I have not specified any rules around data at the path being null and the read succeeds when the value at the path is not null


EDIT - To re-produce this scenario -

1. Upload JSON to the root of a Firebase realtime db-

{
  "users" : {
    "+9198100xxxxx" : {
      "mUser_Id" : "+9198100xxxxx",
      "mUser_ProfilePicUrl" : "url",
      "mUser_Role" : {
        "mRole_Name" : "Employee",
        "mRole_PermissionLevel" : 500
      }
    },
    "+9198100yyyyy" : {
      "mUser_Id" : "+9198100yyyyy",
      "mUser_ProfilePicUrl" : "url",
      "mUser_Role" : {
        "mRole_Name" : "Employee",
        "mRole_PermissionLevel" : 500
      }
    }
  }
}

2. Firebase realtime db rules (actually using for this node) -

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "root.child('users').child($uid).child('mUser_Role').child('mRole_PermissionLevel').val() > 400 || root.child('users').child(auth.token.phone_number).child('mUser_Role').child('mRole_PermissionLevel').val() == 500"
      }
    }
  }
}

3. Use Firbease security rules simulator -

Simulation type - read

Location - /users/+9198100zzzzz (simulated read fails)

Location - /users/+9198100yyyyy (simulated read succeeds)

Authenticated - yes

Provider - custom

Auth token payload -

{
  "provider": "phoneauth",
  "uid": "6f2eed52-b2e9-409a-b5f1-dc307c7f6671",
  "token":{
      "phone_number": "+9198100xxxxx"
  }
}

enter image description here enter image description here

1
Can you update your question to include the minimum JSON and (standalone) code with which you can reproduce the problem? In the code, be sure to print the UID and phone number of the user, right before you try to read the data, so that we have all information necessary to troubleshoot. - Frank van Puffelen
It looks like you're using auth.token.phone_number in your rule instead of auth.uid. Is that intentional? Your question suggests you want to use the uid in the path, and says nothing about phone number. - Doug Stevenson
@DougStevenson Thanks. I am intentionally using auth.token.phone_number in the rule. The user IDs in the database are the same as phone numbers. Also, I am using phone authentication. My security rule gets the phone_number from the auth.token in the request, then checks the permission level at the path corresponding to that user ID in the database (where user ID is same as phone_number).Hence the path in the rule. I am not using auth.uid as a user ID in the database @FrankvanPuffelen Thanks. I'll get back with this as soon as I can - Kartik
@FrankvanPuffelen I have added the minimum JSON and a standalone way to reproduce this using Firebase realtime db security rules simulator (along with the params used in the simulator). Thanks - Kartik
Hmm.... the operation in that last screenshot should also be rejected, shouldn't it? - Frank van Puffelen

1 Answers

4
votes

Non-existent nodes evaluate to null, so your rule essentially evaluates to:

null > 400 || 500 == 500

The rules engine used is strongly typed, which means that null > 400 raises an error instead of evaluating to false.

The solution is to first check for the existence of the node, before comparing its value. So:

".read": "
 (
  root.child('users').child($uid).exists() &&
  root.child('users').child($uid).child('mUser_Role').child('mRole_PermissionLevel').val() > 400
 ) || (
  root.child('users').child(auth.token.phone_number).exists() &&
  root.child('users').child(auth.token.phone_number).child('mUser_Role').child('mRole_PermissionLevel').val() == 500
 )
"