How to secure Firebase Realtime Database rules for in-app messaging between users and business profiles - Stack Overflow

I am very new to coding and I have trouble writing Firebase Realtime Database rules for my project.I a

I am very new to coding and I have trouble writing Firebase Realtime Database rules for my project.

I am using it for real-time in-app messaging.

Before going through the code, let me briefly explain the structure:

  1. Users table

    Users
      |-- user uid (e.g., Wqnxj20ZosOTkfe4D7dwQc9iygp2)
      |     |-- userBusiness1Id : (e.g., 10553)
      |     |-- userBusiness2Id : (e.g., 10552)
      |     |-- userUid : (e.g., Wqnxj20ZosOTkfe4D7dwQc9iygp2)
      |
      |-- user uid (e.g., v6HBc00RL1RQdysCsc09mhKdItD2)
            |-- userUid : (e.g., v6HBc00RL1RQdysCsc09mhKdItD2)
    

    Users can use the app either as standard users or, if they upgrade to pro, they can create up to 2 business profiles. Firebase stores this data accordingly.

  2. User-Messages table

    Description: Messaging can only be done between a standard user and business profiles. To distinguish who is messaging, I use the following format:

    • For business profile: uid_businessId
    • For standard user: uid

    Example identifiers:

    let businessUserId = "Wqnxj20ZosOTkfe4D7dwQc9iygp2_10553"
    let standardUserId = "v6HBc00RL1RQdysCsc09mhKdItD2"
    

    Message Storage: Messages are stored under both the standard user’s and business user’s paths so each can read their own data.

    Example Data Structure: When standardUserId sends a message to businessUserId:

    User-Messages
      |-- businessUserId
      |     |-- standardUserId
      |           |-- chatLogId
      |               |-- fromId: standardUserId
      |               |-- toId: businessUserId
      |
      |-- standardUserId
            |-- businessUserId
                  |-- chatLogId
                      |-- fromId: standardUserId
                      |-- toId: businessUserId
    

Firebase write code:

func sendMessage(text: String, fromId: String, toId: String) {
    let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
    if trimmedText.isEmpty { return }
    
    let timeStamp = Int64(Date().timeIntervalSince1970)
    
    let reference = Database.database().reference().child("User-Messages").child(fromId).child(toId).childByAutoId()
    let toReference = Database.database().reference().child("User-Messages").child(toId).child(fromId).childByAutoId()
    
    let chatModel = ChatModel(chatId: reference.key, text: trimmedText, fromId: fromId, toId: toId, timestamp: timeStamp, messageIsRead: true)
    let chatModelForRecipient = ChatModel(chatId: toReference.key, text: trimmedText, fromId: fromId, toId: toId, timestamp: timeStamp, messageIsRead: false)
    
    reference.setValue(chatModel.toDictionary())
    toReference.setValue(chatModelForRecipient.toDictionary())

}

Problem:

screenshot from firebase

Firebase keeps sending me emails about how my Realtime Database is open for abuse. Below are my current rules for User-Messages:

"User-Messages": {
  "$userId": {
    ".read": "auth != null && (
      auth.uid === $userId || 
      auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId ||
      auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId
    )",
    "$recipientId": {
      ".write": "auth != null && (
        auth.uid === $userId || // Sender can write to their own path
        auth.uid === $recipientId || // Sender can write to the recipient's path
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId || // Business1 sender
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId || // Business2 sender
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $recipientId || // Business1 recipient
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $recipientId // Business2 recipient
      )"
    }
  }
},

I have tried many combinations and even asked for help from ChatGPT, but I couldn't figure this out.

How should I secure my Realtime Database with this relatively complex structure?

I have managed to progress a bit. Now I limited the rules, so a user can read their own messages only (including their business profiles) and send messages will writing for both of the user and the counter party. What is still missing is, when sender is business (uid_businessId), with current rules, Firebase allows only to write it to sender's path, not recipient's path.

I am very new to coding and I have trouble writing Firebase Realtime Database rules for my project.

I am using it for real-time in-app messaging.

Before going through the code, let me briefly explain the structure:

  1. Users table

    Users
      |-- user uid (e.g., Wqnxj20ZosOTkfe4D7dwQc9iygp2)
      |     |-- userBusiness1Id : (e.g., 10553)
      |     |-- userBusiness2Id : (e.g., 10552)
      |     |-- userUid : (e.g., Wqnxj20ZosOTkfe4D7dwQc9iygp2)
      |
      |-- user uid (e.g., v6HBc00RL1RQdysCsc09mhKdItD2)
            |-- userUid : (e.g., v6HBc00RL1RQdysCsc09mhKdItD2)
    

    Users can use the app either as standard users or, if they upgrade to pro, they can create up to 2 business profiles. Firebase stores this data accordingly.

  2. User-Messages table

    Description: Messaging can only be done between a standard user and business profiles. To distinguish who is messaging, I use the following format:

    • For business profile: uid_businessId
    • For standard user: uid

    Example identifiers:

    let businessUserId = "Wqnxj20ZosOTkfe4D7dwQc9iygp2_10553"
    let standardUserId = "v6HBc00RL1RQdysCsc09mhKdItD2"
    

    Message Storage: Messages are stored under both the standard user’s and business user’s paths so each can read their own data.

    Example Data Structure: When standardUserId sends a message to businessUserId:

    User-Messages
      |-- businessUserId
      |     |-- standardUserId
      |           |-- chatLogId
      |               |-- fromId: standardUserId
      |               |-- toId: businessUserId
      |
      |-- standardUserId
            |-- businessUserId
                  |-- chatLogId
                      |-- fromId: standardUserId
                      |-- toId: businessUserId
    

Firebase write code:

func sendMessage(text: String, fromId: String, toId: String) {
    let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
    if trimmedText.isEmpty { return }
    
    let timeStamp = Int64(Date().timeIntervalSince1970)
    
    let reference = Database.database().reference().child("User-Messages").child(fromId).child(toId).childByAutoId()
    let toReference = Database.database().reference().child("User-Messages").child(toId).child(fromId).childByAutoId()
    
    let chatModel = ChatModel(chatId: reference.key, text: trimmedText, fromId: fromId, toId: toId, timestamp: timeStamp, messageIsRead: true)
    let chatModelForRecipient = ChatModel(chatId: toReference.key, text: trimmedText, fromId: fromId, toId: toId, timestamp: timeStamp, messageIsRead: false)
    
    reference.setValue(chatModel.toDictionary())
    toReference.setValue(chatModelForRecipient.toDictionary())

}

Problem:

screenshot from firebase

Firebase keeps sending me emails about how my Realtime Database is open for abuse. Below are my current rules for User-Messages:

"User-Messages": {
  "$userId": {
    ".read": "auth != null && (
      auth.uid === $userId || 
      auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId ||
      auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId
    )",
    "$recipientId": {
      ".write": "auth != null && (
        auth.uid === $userId || // Sender can write to their own path
        auth.uid === $recipientId || // Sender can write to the recipient's path
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId || // Business1 sender
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId || // Business2 sender
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $recipientId || // Business1 recipient
        auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $recipientId // Business2 recipient
      )"
    }
  }
},

I have tried many combinations and even asked for help from ChatGPT, but I couldn't figure this out.

How should I secure my Realtime Database with this relatively complex structure?

I have managed to progress a bit. Now I limited the rules, so a user can read their own messages only (including their business profiles) and send messages will writing for both of the user and the counter party. What is still missing is, when sender is business (uid_businessId), with current rules, Firebase allows only to write it to sender's path, not recipient's path.

Share Improve this question edited Feb 1 at 11:18 Mark Rotteveel 110k229 gold badges156 silver badges225 bronze badges asked Jan 29 at 12:13 S.S.SS.S.S 11 bronze badge 3
  • 1 Firebase security rules are expressions of your application's requirements for data access. Since you haven't said what your requirements are, we can't tell you what your rules should be. There isn't just a simple "right way" to do this, regardless of the structure of your data. You need to start with defining requirements. If your only requirements are that authenticated users can read and write everything in the database, then your rules are just fine, and you can ignore the warning. – Doug Stevenson Commented Jan 29 at 13:19
  • Along with what @DougStevenson mentions about, rules are very challenging for a beginner! They feel complex and the hierarchy seems weird to start with - it took me several months to get comfortable with them. I suggest creating a small sample project to begin with. Keep the depth of your nodes very shallow and craft a rule to allow read/write for auth'd users. Then add another layer; for say, an autoId key, then update your rules. Rinse repeat. It will really help wrap your brain around them and enable you to both craft the needed rules as well as troubleshoot when they are not right – Jay Commented Jan 29 at 16:24
  • thanks for letting me know. I didn't know about how the flow should work here in the posts. I updated the main question with further details. – S.S.S Commented Feb 1 at 10:55
Add a comment  | 

2 Answers 2

Reset to default 0

Unlike relational databases, the right structure for a NoSQL database doesn't depends much on the actual structure of the data. Rather it depends on the use-cases of your app, which typically you won't fully know at the start of your app.


I typically recommend follow the principle of least privilege, which means that your security rules should only allow the minimum access that allows the current use-cases of the app.

{
  "rules": {
    ".read": false,
    ".write": false
  }
}

So before you've written any code, that means that the rules should allow no access whatsoever.


Now you add the code for your first use-case. In your case that seems to be adding a child node under User-Messages.

Note: the Swift code in your question doesn't actually write any database yet. I assume that you want to add a simply text value under the new reference and toReference paths.

Once you run the code with the rules we set in the previous steps it will fails, which is exactly what you want to see.

The rules you have in your question are this:

"User-Messages": {
  ".write": "auth != null",
  ".read": "auth != null"
}

This says that anyone who is signed in can write whatever they want under User-Messages. While allows adding a new messages, it is much broader than what your code actually does. So it does not follow the principle of least privilege.

With your rules, any user can write any message to any other user (likely unwanted). Or worse, they can delete all messages between all users by writing an empty value to the top-level User-Messages node. And even if those are valid use-cases that you want to allow for your app at some point, the current code doesn't need that level of access - so the security rules shouldn't allow it either.

If we model what your code does in the security rules, we end up with something like this:

"User-Messages": {
  "$fromUid": {
    "$toUid": {
      "$pushId": {
        ".write": "auth != null && (auth.uid === fromUid || auth.uid === toUid)"
      }
    }
  }
}

I hope you can see how closely this mimics your code: this uses wildcard variables so that a user can now only write a message under the full path /User-Messages/$fromUid/toUid/$pushId, which is exactly what your code does.

More so, the write is only allowed if their UID matches either one of the parent keys, which I assume is the use-case you want.


You'll typically also want to add some data validation to the security rules, like the maximum length of the message and maybe who can send a message to whom.

Again, as said above, the goal here is to ensure that the security rules only allow exactly what your code currently does and needs and nothing more.


Once you're done with all of these, move on the code for your next use-case.

Got the solution now. Sharing for anyone who might get benefit of the approach. Thanks

{
  "rules": {
    ".read": false, // disables general read access
    ".write": false, // disables general write access

    "Users": {
      ".read": "auth != null", // Users can read all profiles
      
      "$userUid": {
        ".write": "auth != null && auth.uid === $userUid", // Users can write only their own data
        
        ".validate": "newData.child('userUid').val() === auth.uid && 
              
              (!newData.child('userID').exists() || newData.child('userID').isNumber()) &&
              (!newData.child('userName').exists() || 
               (newData.child('userName').isString() && newData.child('userName').val().length > 0)) &&
              (!newData.child('userFCMToken').exists() || 
               (newData.child('userFCMToken').isString() && newData.child('userFCMToken').val().length > 0)) &&
              (!newData.child('userPhoneLanguage').exists() || newData.child('userPhoneLanguage').isString()) &&

              (!newData.child('userProfileImage').exists() || 
               (newData.child('userProfileImage').isString() && 
                newData.child('userProfileImage').val().matches(/^https:\\/\\/xxxx\\//))) &&

              (!newData.child('userBusiness1Id').exists() || newData.child('userBusiness1Id').isNumber()) &&
              (!newData.child('userBusiness1Name').exists() || 
               (newData.child('userBusiness1Name').isString() && newData.child('userBusiness1Name').val().length > 0)) &&
              (!newData.child('userBusiness1ProfileImage').exists() || 
               (newData.child('userBusiness1ProfileImage').isString() && 
                newData.child('userBusiness1ProfileImage').val().matches(/^https:\\/\\/xxx\\//))) &&

              (!newData.child('userBusiness2Id').exists() || newData.child('userBusiness2Id').isNumber()) &&
              (!newData.child('userBusiness2Name').exists() || 
               (newData.child('userBusiness2Name').isString() && newData.child('userBusiness2Name').val().length > 0)) &&
              (!newData.child('userBusiness2ProfileImage').exists() || 
               (newData.child('userBusiness2ProfileImage').isString() && 
                newData.child('userBusiness2ProfileImage').val().matches(/^https:\\/\\/xxxx\\//)))"
      }
    },
        "User-Messages": {
      "$userId": {
        ".read": "auth != null && (
          auth.uid === $userId || 
          auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId ||
          auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId
        )",
        "$recipientId": {
          ".write": "auth != null && (
            auth.uid === $userId || // Sender can write to their own path
            auth.uid === $recipientId || // Sender can write to the recipient's path
            $recipientId === auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() ||  // When sender is business1, sender can write to user's path
            $recipientId === auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() ||  // When sender is business2, sender can write to user's path
            auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $userId || // Business1 sender
            auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $userId // Business2 sender
          )",
        "$messageId": {
            ".validate": "newData.child('chatId').exists() && newData.child('chatId').isString() && newData.child('chatId').val().length > 0 &&
                          newData.child('fromId').exists() && newData.child('fromId').isString() && newData.child('fromId').val().length > 0 &&
                          newData.child('toId').exists() && newData.child('toId').isString() && newData.child('toId').val().length > 0 &&
                          newData.child('timestamp').exists() && newData.child('timestamp').isNumber() && newData.child('timestamp').val() > 0 &&
                          newData.child('text').exists() && newData.child('text').isString() && 
                          newData.child('text').val().length > 0 &&
                          newData.child('messageIsRead').exists() && newData.child('messageIsRead').isBoolean() &&
                          (!newData.child('membershipRequest').exists() || (newData.child('membershipRequest').isString()))"
          }
        }
      }
    },

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745298870a4621311.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信