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;
}
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?
