0

I have chat documents like the following:

{
    members: ['abc','def'] // 2 element list of member UIDs
    // some other chat metadata
}

In firestore, I have the following rules on my collection:

function authed() {
  return request.auth != null;
}

match /chats/{chatId} {

  allow create: if authed()
    && request.resource.data.members is list
    && request.resource.data.members.size() == 2
    && request.auth.uid in request.resource.data.members;


  allow read, update: if authed()
    && resource.data.members is list
    && request.auth.uid in resource.data.members;
    
  allow list: if authed();

The last clause (allow list: if authed();) is necessary to allow users to run the following query on my client app:

    const chatsRef = collection(firestoreDb, 'chats');

    const LAST_MESSAGE_AT_PROPERTY: keyof Chat = 'lastMessageAt';
    const MEMBER_PROPERTY: keyof Chat = 'members';
    const q = query(
      chatsRef,
      where('members', 'array-contains', userId),
      orderBy('lastMessageAt', 'desc'),
      limit(20)
    );

Without it, I would get a firebase error. However, this rule still permits any user to pull any other user's chats (I tested it by manually substituting another user's UID for userId).

From what I've read, on this last clause, there's no way I can inspect the actual query's where clause and ensure userId matches the request.auth.uid, and that the read rule that applies to each individual matched document should prevent the read of another user's chats, but this doesn't seem to be happening for me.

Could someone help tell me what I've got wrong in my rule? Here was the actual payload for this call:

{
  "database": "projects/my-project/databases/(default)",
  "addTarget": {
    "query": {
      "structuredQuery": {
        "from": [{ "collectionId": "chats" }],
        "where": {
          "fieldFilter": {
            "field": { "fieldPath": "members" },
            "op": "ARRAY_CONTAINS",
            "value": { "stringValue": "5Sdmw2sxJBdp0YnW3LtqgfYMrW83" }
          }
        },
        "orderBy": [
          {
            "field": { "fieldPath": "lastMessageAt" },
            "direction": "DESCENDING"
          },
          { "field": { "fieldPath": "__name__" }, "direction": "DESCENDING" }
        ],
        "limit": 20
      },
      "parent": "projects/my-project/databases/(default)/documents"
    },
    "targetId": 2
  }
}

And this was the decoded bearer token it was sent with:

enter image description here

However, when I replace the above userId with another user's UID ('5Sdmw2sxJBdp0YnW3LtqgfYMrW83', who I'm not authenticated as), here's an example document returned which should not be returned as it should be a private conversation between two other users (NOTE: The screen shot shows memberUids instead of members, as I renamed the property, but assume that the value is called members in the photo to coincide with all the code snippets previously provided ): enter image description here

2
  • We have no way to verify the values of MEMBERS_PROPERTY, LAST_MESSAGE_AT_PROPERTY and userId. Can you reproduce with hard-coded values, or Firebase API calls for those? If so, please edit your question to use those. --- Same for chatsRef - please show how that is initialized.= Commented Nov 29, 2025 at 23:59
  • Explanation of what should be happening below. If that is not what you see, please update your question to show the actual query you run (with hard-coded values), and a screenshot of a document that you're getting back that does not match the requirement of the rules. Commented Nov 30, 2025 at 0:06

1 Answer 1

0

It sounds like you want to require that the user always passes their own UID for the userId in your code.

You already validate that value in the rules here:

request.auth.uid in request.resource.data.members;

This condition ensures that the user can only request documents where their own UID (request.auth.uid) is present in the members array field. If they pass another value than their own UID, the clause will reject the operation.


If your code (or that of a malicious user) were to pass a different value for the UID, that query will end up requesting documents without checking for its own UID - and the rules will reject that case.


Note that you say:

the read rule that applies to each individual matched document

This is actually not how Firestore security rules work: they don't check each individual document, as that would not scale. Rather, they ensure that the operation you perform can only access information that the rules allow.

That is also why this scenario should work fine: if you pass your own UID, the rules engine can validate that against the request.auth.uid above. If you pass someone else's UID, it won't match the clause.

Sign up to request clarification or add additional context in comments.

3 Comments

Hello Frank, I updated my original question to provide some more context, hopefully it's what you meant. The clause you pointed out does have the desired behavior when accessing an individual chat document, however, not when I am querying the collection for a list of matched documents (i.e. all chats the requesting user is a member of). So what I want is that for the request to be rejected if the userId in where('members', 'array-contains', userId) is not equal to the request.auth.uid, but docs say rules can't inspect the where clause so idk how to block this
Yeah... so I've never actually made the list scenario work myself. I always end up with a separate document that contains all the chats that the user is a member of. So I always store the same data twice, once as a list of member IDs in each specific chat document (as you have) and once as a list of chat IDs for a specific user (that you're missing).
Note: if you're passing another user's UID, the request.auth.uid in request.resource.data.members clause should reject the list call. What SDK/API are you using to make the list call/query, and how are you authenticated there?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.