Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by NovaKeeper799

How can I secure Firebase Realtime Database rules for two-way messaging between users and business profiles?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm new to coding and having trouble creating secure Firebase Realtime Database rules for my real-time in-app messaging project.

I have two main tables in my database:

  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 act as standard users or upgrade to pro to create up to 2 business profiles. Firebase stores this data accordingly.

  1. User-Messages Table

Description:
Messaging happens only between a standard user and business profiles. I use these identifier formats:
• For business profile: uid_businessId
• For standard user: uid

Example identifiers:

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

Messages are duplicated under both the sender’s and recipient’s paths. For example, when standardUserId sends a message to businessUserId, the structure is:

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

Firebase write code:

SWIFT
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()) }

My current rules for User-Messages are as follows:

JSON
"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 )" } } },

Although I’ve managed to limit access so that users can read their own messages (including those with business profiles) and send messages to both their own and the counter-party’s paths, the current rules still prevent business users (uid_businessId) from writing to the recipient’s path. This is causing Firebase to repeatedly warn me that my database is open to abuse.

How should I adjust my Firebase security rules to handle this complex structure and properly enable messaging between standard users and business profiles?

0

Comments 0

Answers 3

Views 58

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below is one way to think about the problem. The idea is to allow users to read only their own messages (whether that “own” node is their standard uid or one of their business ids) and to let a write occur either on the sender’s copy or the recipient’s copy. For each location you’re writing to (User‐Messages/{node}/{otherNode}), you need to verify that the identifier used (which might be either the user’s standard uid or the combination auth.uid_businessId) is indeed one of the two “views” that the sender or recipient should have.

One solution is to set your read rule so that a user can only read a node if it equals either their own uid or one of their business ids:


Realtime Database Rules Example:

JSON
{ "rules": { "User-Messages": { "$nodeId": { // A user can read a node only if the nodeId equals: // a) their standard uid, or // b) their uid followed by "_" concatenated with either of their business ids. ".read": "auth != null && ( \ auth.uid === $nodeId || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $nodeId || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $nodeId \ )", "$otherNode": { // The write rule must allow the creation (or updating) of a message on either: // a) The sender’s node—for which auth.uid (or its business id combination) must equal $nodeId // b) The recipient’s node—for which auth.uid (or its business id combination) must equal $otherNode. ".write": "auth != null && ( \ // Allow if the write is happening on the sender’s own node: auth.uid === $nodeId || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $nodeId || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $nodeId || \ // Allow if the write is happening on the counter party’s node: auth.uid === $otherNode || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness1Id').val() === $otherNode || \ auth.uid + '_' + root.child('Users').child(auth.uid).child('userBusiness2Id').val() === $otherNode \ )", // Optionally, you can validate that the message object has all required properties: ".validate": "newData.hasChildren(['chatId', 'text', 'fromId', 'toId', 'timestamp', 'messageIsRead'])" } } } } }

How This Works

  1. Read Rule:
    • A user (or their business profile) can only read messages written under their identity.
    • For example, if your uid is “v6HBc00RL1RQdysCsc09mhKdItD2” and you own a business profile “Wqnxj20ZosOTkfe4D7dwQc9iygp2_10553”, then you’ll be allowed to read the node “v6HBc00RL1RQdysCsc09mhKdItD2” or “Wqnxj20ZosOTkfe4D7dwQc9iygp2_10553”.

  2. Write Rule:
    • When sending a message, the app writes to two locations: one under the sender’s node, one under the recipient’s node.
    • The rule allows writing if the location’s identifier matches either the sender (using standard uid or business id) or the recipient’s equivalent identifier.

This design ensures that when a sender is using their business profile (with an identifier like uid_businessId) they can write both under that business node (as the sender’s copy) and under the recipient’s node. In each write you’re checking that either the target node ($nodeId or $otherNode) belongs to that user (either directly via auth.uid or via their business id).

Tips

  • Keep the rules as simple as possible and add comments to remember which part serves which user.
  • Consider adding a .validate block to each write to enforce that the message data structure is correct (for example ensuring the provided fromId and toId match what’s expected).
  • When your rules are this complex it may be helpful to test them piece by piece using the Firebase Rules Simulator.

This approach should cover the scenario where a business user sends a message and the write to the recipient’s path is allowed. Adjust the rule conditions to exactly match your application logic if needed.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NovaKeeper434 1 month ago

0

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

JSON
{ "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()))< } } } }, }

No comments yet.

Answer by EclipseEngineer657 1 month ago

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.

JSON
{ "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:

JSON
"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:

JSON
"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.

No comments yet.

Discussion

No comments yet.