Firestore Rules: resource.data.keys() doesn't contain all fields in read

Viewed 510

The Setup


I have a collection in Firebase Firestore that has the following fields:

(["active" , "created" , "description" , "displayName" , "expires" , "image" , "type" , "uid" , "userName"])

where "expires" is optional.

The write rules ensure that every object follows that form and are tested successfully.

The Problem


When trying to read from the collection, I have a rule that states the following:

let seeUnexpired = !("expires" in resource.data.keys()) ||
resource.data.expires > request.time ||
request.auth.uid == resource.data.uid;

That prohibits users other than the author from reading expired entries. This rule was not prohibiting reads, however. I am testing using a local emulator with carefully curated data and am confident that the expire field exists and is outdated for this test.

Details


In attempt to debug I discovered that the condition causing the rule to fail was !("expires" in resource.data.keys()).

Running the tests with debug(resource.data.keys()) printed out this object to firestore-debug.log:

list_value {
  values {
    string_value: "active"
  }
}

where "active" only shows up when using this query condition in the request: ...collection('collection_name').where('active', '==', true).get().

This indicates to me that resource.data.keys() only includes the fields from the resource that are referenced in where clauses on the read request. That means a request can circumvent a "field must not exist" rule by simply not including it in a query.

Debug:

Mar 31, 2021 12:34:48 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
INFO: Detected non-HTTP/2 connection.
list_value {
  values {
    string_value: "active"
  }
}

Tests


Test:

await firestoreAdmin.collection(COLLECTIONS.items).doc(mockItem.id).update({active: true, expires: new Date('01 Jan 2000 00:00:00 GMT')});
const query = firestore.collection(COLLECTIONS.items).where('active', '==', true); //unAuthed firestore instance
await assertFails(query.get());

Rule:

function readItemRules() {
  let seeActive = resource.data.active == true || request.auth.uid == resource.data.uid;
  let seeUnexpired = !("expires" in resource.data.keys()) ||
  resource.data.expires > request.time ||
  request.auth.uid == resource.data.uid; TODO figure out expiration rules
  return seeActive && seeUnexpired;
}

Data: Firestore Emulator Data

The Question


Is my understanding of this problem accurate, or am I missing a detail or syntax quirk? Is this behavior intentional, and if so how should I modify my rules/data to enforce this kind of security?

1 Answers

My discovery was that for a read rule, firestore only seems to load the fields from the document which are mentioned in the query. This makes a lot of sense for rules that require the field to exist or match something, because if you don't include it in a query they're guaranteed to fail (as they will be tested against something undefined). It just doesn't work here because if you omit it from the query it'll pass the does not exist rule no matter what since firestore omits that field from the whole resource when doing the check.

My solution was simply to add another, boolean field "hasExpiration" that indicates whether or not the "expires" field will be present. I am a little unsatisfied with this solution because it adds complexity and falls on the client/write rules to ensure that there is parity between those fields.

https://firebase.google.com/docs/firestore/security/rules-query#rules_are_not_filters

https://firebase.google.com/docs/firestore/security/rules-structure#granular_operations

Related